From 9c3814df9c2472b8261e03df8558b3380fbf595b Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 6 Aug 2020 16:32:14 +1000 Subject: [PATCH 01/38] Implement shared sender keys database --- .../database/helpers/SQLCipherOpenHelper.java | 11 +- .../loki/database/SharedSenderKeysDatabase.kt | 105 ++++++++++++++++++ .../loki/utilities/DatabaseUtilities.kt | 4 +- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 2dc877906d..14708f9f38 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; 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.notifications.NotificationChannels; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.GroupUtil; @@ -85,8 +86,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV9 = 30; private static final int lokiV10 = 31; private static final int lokiV11 = 32; + private static final int lokiV12 = 33; - private static final int DATABASE_VERSION = lokiV11; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV12; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -157,6 +159,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetsTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeysTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -603,6 +607,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand()); } + if (oldVersion < lokiV12) { + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetsTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeysTableCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt new file mode 100644 index 0000000000..749f4ba02f --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.loki.database + +import android.content.ContentValues +import android.content.Context +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.loki.utilities.* +import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol + +class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol { + + companion object { + // Shared + private val closedGroupPublicKey = "closed_group_public_key" + // Ratchets + private val closedGroupRatchetsTable = "closed_group_ratchets" + private val senderPublicKey = "sender_public_key" + private val chainKey = "chain_key" + private val keyIndex = "key_index" + private val messageKeys = "message_keys" + @JvmStatic val createClosedGroupRatchetsTableCommand + = "CREATE TABLE $closedGroupRatchetsTable (PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey), $chainKey STRING, $keyIndex INTEGER DEFAULT 0, $messageKeys STRING);" + // Private keys + private val closedGroupPrivateKeysTable = "closed_group_private_keys" + private val closedGroupPrivateKey = "closed_group_private_key" + @JvmStatic val createClosedGroupPrivateKeysTableCommand + = "CREATE TABLE $closedGroupPrivateKeysTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);" + } + + // region Ratchets & Sender Keys + override fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet? { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + return database.get(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + val chainKey = cursor.getString(Companion.chainKey) + val keyIndex = cursor.getInt(Companion.keyIndex) + val messageKeys = cursor.getString(Companion.messageKeys).split("-") + ClosedGroupRatchet(chainKey, keyIndex, messageKeys) + } + } + + override fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet) { + val database = databaseHelper.writableDatabase + val values = ContentValues() + values.put(Companion.closedGroupPublicKey, groupPublicKey) + values.put(Companion.senderPublicKey, senderPublicKey) + values.put(Companion.chainKey, ratchet.chainKey) + values.put(Companion.keyIndex, ratchet.keyIndex) + values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-")) + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + database.insertOrUpdate(closedGroupRatchetsTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) + } + + override fun removeAllClosedGroupRatchets(groupPublicKey: String) { + val database = databaseHelper.writableDatabase + database.delete(closedGroupRatchetsTable, null, null) + } + + override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + return database.getAll(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + val chainKey = cursor.getString(Companion.chainKey) + val keyIndex = cursor.getInt(Companion.keyIndex) + val senderPublicKey = cursor.getString(Companion.senderPublicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(chainKey), keyIndex, Hex.fromStringCondensed(senderPublicKey)) + }.toSet() + } + // endregion + + // region Public & Private Keys + override fun getClosedGroupPrivateKey(groupPublicKey: String): String? { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ?" + return database.get(closedGroupPrivateKeysTable, query, arrayOf( groupPublicKey )) { cursor -> + cursor.getString(Companion.closedGroupPrivateKey) + } + } + + override fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String) { + val database = databaseHelper.writableDatabase + val values = ContentValues() + values.put(Companion.closedGroupPublicKey, groupPublicKey) + values.put(Companion.closedGroupPrivateKey, groupPrivateKey) + val query = "${Companion.closedGroupPublicKey} = ?" + database.insertOrUpdate(closedGroupPrivateKeysTable, values, query, arrayOf( groupPublicKey )) + } + + override fun removeClosedGroupPrivateKey(groupPublicKey: String) { + val database = databaseHelper.writableDatabase + val query = "${Companion.closedGroupPublicKey} = ?" + database.delete(closedGroupPrivateKeysTable, query, arrayOf( groupPublicKey )) + } + + override fun getAllClosedGroupPublicKeys(): Set { + val database = databaseHelper.readableDatabase + return database.getAll(closedGroupPrivateKeysTable, null, null) { cursor -> + cursor.getString(Companion.closedGroupPublicKey) + }.toSet() + } + // endregion +} diff --git a/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt index 8d3112939b..33d8997f02 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt @@ -5,7 +5,7 @@ import net.sqlcipher.Cursor import net.sqlcipher.database.SQLiteDatabase import org.whispersystems.signalservice.internal.util.Base64 -fun SQLiteDatabase.get(table: String, query: String, arguments: Array, get: (Cursor) -> T): T? { +fun SQLiteDatabase.get(table: String, query: String?, arguments: Array?, get: (Cursor) -> T): T? { var cursor: Cursor? = null try { cursor = query(table, null, query, arguments, null, null, null) @@ -18,7 +18,7 @@ fun SQLiteDatabase.get(table: String, query: String, arguments: Array SQLiteDatabase.getAll(table: String, query: String, arguments: Array, get: (Cursor) -> T): List { +fun SQLiteDatabase.getAll(table: String, query: String?, arguments: Array?, get: (Cursor) -> T): List { val result = mutableListOf() var cursor: Cursor? = null try { From c2d1953116299e47516a70946d96eb43127a3357 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 09:15:36 +1000 Subject: [PATCH 02/38] Clean ClosedGroupsProtocol ahead of SSK changes --- .../conversation/ConversationActivity.java | 2 +- .../groups/GroupMessageProcessor.java | 5 -- .../securesms/jobs/PushDecryptJob.java | 2 +- .../securesms/jobs/PushGroupSendJob.java | 2 +- .../securesms/loki/activities/HomeActivity.kt | 3 +- .../loki/protocol/ClosedGroupsProtocol.kt | 62 +++++++++---------- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index d2a0122edf..34aeb1c16b 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1166,7 +1166,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)); builder.setPositiveButton(R.string.yes, (dialog, which) -> { Recipient groupRecipient = getRecipient(); - if (ClosedGroupsProtocol.leaveGroup(this, groupRecipient)) { + if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { initializeEnabledCheck(); } else { Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 849cc67a32..f7c9bb9de6 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -97,11 +97,6 @@ public class GroupMessageProcessor { } } - // Loki - Ignore message if needed - if (ClosedGroupsProtocol.shouldIgnoreGroupCreatedMessage(context, group)) { - return null; - } - // Loki - Parse admins if (group.getAdmins().isPresent()) { for (String admin : group.getAdmins().get()) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 6183467232..1e3988f1e0 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -1454,7 +1454,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; - boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation, groupId.orNull(), content); + boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation.getAddress(), groupId.orNull(), content.getSender()); return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && shouldIgnoreContentMessage); } else { return sender.isBlocked(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 0a98ebddc0..f240ec24a2 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -154,7 +154,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress)); else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList(); - else targets = ClosedGroupsProtocol.getDestinations(message.getRecipient().getAddress().toGroupString(), context).get(); + else targets = ClosedGroupsProtocol.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString()); List results = deliver(message, targets); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList(); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index cc2c3e5793..1e55c52e8c 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob -import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol @@ -333,7 +332,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { - if (!ClosedGroupsProtocol.leaveGroup(this, recipient)) { + if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) { Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() return@setPositiveButton } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 8bcff5516d..ea3f4acd7c 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context +import android.util.Log import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext @@ -24,39 +25,27 @@ import java.util.* object ClosedGroupsProtocol { - /** - * Blocks the calling thread. - */ @JvmStatic - fun shouldIgnoreContentMessage(context: Context, conversation: Recipient, groupID: String?, content: SignalServiceContent): Boolean { - if (!conversation.address.isClosedGroup || groupID == null) { return false } - // A closed group's members should never include slave devices - val senderPublicKey = content.sender + fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { + if (!address.isClosedGroup || groupID == null) { return false } + /* FileServerAPI.shared.getDeviceLinks(senderPublicKey).timeout(6000).get() val senderMasterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(senderPublicKey) val publicKeyToCheckFor = senderMasterPublicKey ?: senderPublicKey + */ val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true) - return !members.contains(recipient(context, publicKeyToCheckFor)) + return !members.contains(recipient(context, senderPublicKey)) } @JvmStatic - fun shouldIgnoreGroupCreatedMessage(context: Context, group: SignalServiceGroup): Boolean { - val members = group.members - val masterPublicKeyOrNull = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - val masterPublicKey = masterPublicKeyOrNull ?: TextSecurePreferences.getLocalNumber(context) - return !members.isPresent || !members.get().contains(masterPublicKey) - } - - @JvmStatic - fun getDestinations(groupID: String, context: Context): Promise, Exception> { - if (GroupUtil.isRSSFeed(groupID)) { return Promise.of(listOf()) } + fun getMessageDestinations(context: Context, groupID: String): List
{ + if (GroupUtil.isRSSFeed(groupID)) { return listOf() } if (GroupUtil.isOpenGroup(groupID)) { - val result = mutableListOf
() - result.add(Address.fromSerialized(groupID)) - return Promise.of(result) + return listOf( Address.fromSerialized(groupID) ) } else { - // A closed group's members should never include slave devices - val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false) + // TODO: Shared sender keys + return DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address } + /* return FileServerAPI.shared.getDeviceLinks(members.map { it.address.serialize() }.toSet()).map { val result = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) } @@ -71,29 +60,33 @@ object ClosedGroupsProtocol { } result.toList() } + */ } } @JvmStatic - fun leaveGroup(context: Context, recipient: Recipient): Boolean { + fun leaveLegacyGroup(context: Context, recipient: Recipient): Boolean { if (!recipient.address.isClosedGroup) { return true } val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) - val message = GroupUtil.createGroupLeaveMessage(context, recipient) - if (threadID < 0 || !message.isPresent) { return false } - MessageSender.send(context, message.get(), threadID, false, null) - // Remove the master device from the group (a closed group's members should never include slave devices) + val message = GroupUtil.createGroupLeaveMessage(context, recipient).orNull() + if (threadID < 0 || message == null) { return false } + MessageSender.send(context, message, threadID, false, null) + /* val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context) + */ + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDatabase = DatabaseFactory.getGroupDatabase(context) val groupID = recipient.address.toGroupString() groupDatabase.setActive(groupID, false) - groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove)) + groupDatabase.remove(groupID, Address.fromSerialized(userPublicKey)) return true } @JvmStatic fun establishSessionsWithMembersIfNeeded(context: Context, members: List) { - // A closed group's members should never include slave devices + @Suppress("NAME_SHADOWING") val members = members.toMutableSet() + /* val allDevices = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member) }.toMutableSet() @@ -101,12 +94,13 @@ object ClosedGroupsProtocol { if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) { allDevices.remove(userMasterPublicKey) } + */ val userPublicKey = TextSecurePreferences.getLocalNumber(context) - if (userPublicKey != null && allDevices.contains(userPublicKey)) { - allDevices.remove(userPublicKey) + if (userPublicKey != null && members.contains(userPublicKey)) { + members.remove(userPublicKey) } - for (device in allDevices) { - ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(device) + for (member in members) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(member) } } } \ No newline at end of file From a5a53adc479ee587624de347f7f5944176d33709 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 09:20:22 +1000 Subject: [PATCH 03/38] Re-order files --- .../securesms/dependencies/SignalCommunicationModule.java | 2 +- src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java | 2 +- .../securesms/jobs/MultiDeviceContactUpdateJob.java | 2 +- src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 4 ++-- .../securesms/loki/activities/JoinPublicChatActivity.kt | 2 +- .../securesms/loki/activities/LandingActivity.kt | 2 +- .../securesms/loki/activities/LinkedDevicesActivity.kt | 2 +- .../securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt | 2 +- .../protocol/{ => shelved}/MultiDeviceOpenGroupUpdateJob.kt | 5 +++-- .../loki/protocol/{ => shelved}/MultiDeviceProtocol.kt | 4 ++-- .../loki/protocol/{ => shelved}/SyncMessagesProtocol.kt | 3 +-- .../securesms/notifications/MarkReadReceiver.java | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) rename src/org/thoughtcrime/securesms/loki/protocol/{ => shelved}/MultiDeviceOpenGroupUpdateJob.kt (96%) rename src/org/thoughtcrime/securesms/loki/protocol/{ => shelved}/MultiDeviceProtocol.kt (98%) rename src/org/thoughtcrime/securesms/loki/protocol/{ => shelved}/SyncMessagesProtocol.kt (99%) diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 79a875c817..bb62e86933 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.push.MessageSenderEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index be5aef3962..aa628dc7cc 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob; diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 1a012b7270..184c515c12 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKey; diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 1e3988f1e0..85c9e6e48f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -66,11 +66,11 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol; 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.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.loki.utilities.PromiseUtilities; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; diff --git a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index fcf3e9e4e7..11f3a79b06 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { diff --git a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt index 8cd45d95f0..9ee7bf81c2 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialog import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialogDelegate import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo import org.thoughtcrime.securesms.loki.utilities.show diff --git a/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt index 041e8a8efa..2c4e9dabde 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.devicelist.Device import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.loki.dialogs.* -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage diff --git a/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt b/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt index fbb476e527..7dd30cf323 100644 --- a/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt +++ b/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt @@ -15,7 +15,7 @@ import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.QRCodeUtilities import org.thoughtcrime.securesms.loki.utilities.toPx diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt similarity index 96% rename from src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt index 70de694044..eeb7baabf6 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.database.DatabaseFactory @@ -32,7 +32,8 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) .setMaxAttempts(Parameters.UNLIMITED) .build()) - override fun getFactoryKey(): String { return KEY } + override fun getFactoryKey(): String { return KEY + } override fun serialize(): Data { return Data.EMPTY } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt similarity index 98% rename from src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt index 4d37ab1e58..130dd2b9b3 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import android.content.Context import android.util.Log @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.PushMediaSendJob import org.thoughtcrime.securesms.jobs.PushSendJob import org.thoughtcrime.securesms.jobs.PushTextSendJob +import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol import org.thoughtcrime.securesms.loki.utilities.Broadcaster import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.recipients.Recipient @@ -19,7 +20,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI -import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLink import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLinkingSession import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt similarity index 99% rename from src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt index 5ec37a41dd..487dfe7251 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import android.content.Context import android.util.Log @@ -28,7 +28,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInp import org.whispersystems.signalservice.loki.api.opengroups.PublicChat import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation -import java.util.* object SyncMessagesProtocol { diff --git a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 942de714c3..ca8271a3f9 100644 --- a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol; From 944f85ddb94a4516ca6453ce8166ef5a0ac64d14 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 10:17:35 +1000 Subject: [PATCH 04/38] Partially implement SSK group creation --- .../securesms/database/DatabaseFactory.java | 57 +++++++++++-------- .../loki/protocol/ClosedGroupsProtocol.kt | 50 ++++++++++++---- .../PushSessionRequestMessageSendJob.kt | 14 ++--- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 4465a5c1c0..d945967507 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; 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.util.TextSecurePreferences; public class DatabaseFactory { @@ -73,6 +74,7 @@ public class DatabaseFactory { private final LokiMessageDatabase lokiMessageDatabase; private final LokiThreadDatabase lokiThreadDatabase; private final LokiUserDatabase lokiUserDatabase; + private final SharedSenderKeysDatabase sskDatabase; public static DatabaseFactory getInstance(Context context) { synchronized (lock) { @@ -187,6 +189,10 @@ public class DatabaseFactory { public static LokiUserDatabase getLokiUserDatabase(Context context) { return getInstance(context).lokiUserDatabase; } + + public static SharedSenderKeysDatabase getSSKDatabase(Context context) { + return getInstance(context).sskDatabase; + } // endregion public static void upgradeRestored(Context context, SQLiteDatabase database){ @@ -200,32 +206,33 @@ public class DatabaseFactory { DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret(); AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); - this.sms = new SmsDatabase(context, databaseHelper); - this.mms = new MmsDatabase(context, databaseHelper); - this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); - this.media = new MediaDatabase(context, databaseHelper); - this.thread = new ThreadDatabase(context, databaseHelper); - this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); - this.identityDatabase = new IdentityDatabase(context, databaseHelper); - this.draftDatabase = new DraftDatabase(context, databaseHelper); - this.pushDatabase = new PushDatabase(context, databaseHelper); - this.groupDatabase = new GroupDatabase(context, databaseHelper); - this.recipientDatabase = new RecipientDatabase(context, databaseHelper); - this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); - this.contactsDatabase = new ContactsDatabase(context); - this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); - this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); - this.sessionDatabase = new SessionDatabase(context, databaseHelper); - this.searchDatabase = new SearchDatabase(context, databaseHelper); - this.jobDatabase = new JobDatabase(context, databaseHelper); - this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); - this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper); + this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); + this.sms = new SmsDatabase(context, databaseHelper); + this.mms = new MmsDatabase(context, databaseHelper); + this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); + this.media = new MediaDatabase(context, databaseHelper); + this.thread = new ThreadDatabase(context, databaseHelper); + this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); + this.identityDatabase = new IdentityDatabase(context, databaseHelper); + this.draftDatabase = new DraftDatabase(context, databaseHelper); + this.pushDatabase = new PushDatabase(context, databaseHelper); + this.groupDatabase = new GroupDatabase(context, databaseHelper); + this.recipientDatabase = new RecipientDatabase(context, databaseHelper); + this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); + this.contactsDatabase = new ContactsDatabase(context); + this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); + this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); + this.sessionDatabase = new SessionDatabase(context, databaseHelper); + this.searchDatabase = new SearchDatabase(context, databaseHelper); + this.jobDatabase = new JobDatabase(context, databaseHelper); + this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); + this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper); this.lokiContactPreKeyDatabase = new LokiPreKeyRecordDatabase(context, databaseHelper); - this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper); - this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper); - this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper); - this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper); + this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper); + this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper); + this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper); + this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper); + this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index ea3f4acd7c..522bbce2f5 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -1,30 +1,56 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context -import android.util.Log import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.utilities.recipient -import org.thoughtcrime.securesms.loki.utilities.timeout import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender +import org.thoughtcrime.securesms.sms.OutgoingTextMessage import org.thoughtcrime.securesms.util.GroupUtil +import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.libsignal.SignalProtocolAddress -import org.whispersystems.signalservice.api.messages.SignalServiceContent -import org.whispersystems.signalservice.api.messages.SignalServiceGroup -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.loki.api.SnodeAPI -import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI -import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey +import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey import java.util.* object ClosedGroupsProtocol { + public fun createClosedGroup(context: Context, name: String, members: Collection): Promise { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + // Generate a key pair for the group + val groupKeyPair = Curve.generateKeyPair() + val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + 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)) + } + // Create the group + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false); + val admins = setOf( Address.fromSerialized(userPublicKey) ) + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins)) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send a closed group update message to all members using established channels + // TODO + // Add the group to the user's set of public keys to poll for + DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) + // Notify the user + // TODO + // Return + return Promise.of(Unit) + } + @JvmStatic fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { if (!address.isClosedGroup || groupID == null) { return false } @@ -84,7 +110,7 @@ object ClosedGroupsProtocol { } @JvmStatic - fun establishSessionsWithMembersIfNeeded(context: Context, members: List) { + fun establishSessionsWithMembersIfNeeded(context: Context, members: Collection) { @Suppress("NAME_SHADOWING") val members = members.toMutableSet() /* val allDevices = members.flatMap { member -> diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt index f530468235..01b2668f71 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt @@ -26,13 +26,13 @@ class PushSessionRequestMessageSendJob private constructor(parameters: Parameter } constructor(publicKey: String, timestamp: Long) : this(Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(1) - .build(), - publicKey, - timestamp) + .addConstraint(NetworkConstraint.KEY) + .setQueue(KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(1) + .build(), + publicKey, + timestamp) override fun serialize(): Data { return Data.Builder().putString("publicKey", publicKey).putLong("timestamp", timestamp).build() From 15f394283860dcee1a774348754f031d59b20a04 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 10:53:36 +1000 Subject: [PATCH 05/38] Clean --- src/org/thoughtcrime/securesms/ApplicationContext.java | 4 ++-- .../jobmanager/migration/WorkManagerFactoryMappings.java | 4 ++-- .../thoughtcrime/securesms/jobs/JobManagerFactories.java | 8 ++++---- .../{PushNullMessageSendJob.kt => NullMessageSendJob.kt} | 8 ++++---- .../securesms/loki/protocol/SessionManagementProtocol.kt | 4 ++-- ...tMessageSendJob.kt => SessionRequestMessageSendJob.kt} | 8 ++++---- .../securesms/loki/protocol/SessionResetImplementation.kt | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) rename src/org/thoughtcrime/securesms/loki/protocol/{PushNullMessageSendJob.kt => NullMessageSendJob.kt} (89%) rename src/org/thoughtcrime/securesms/loki/protocol/{PushSessionRequestMessageSendJob.kt => SessionRequestMessageSendJob.kt} (92%) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index ae30fce7b2..0903e3c959 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -66,7 +66,7 @@ import org.thoughtcrime.securesms.loki.api.PublicChatManager; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; @@ -621,7 +621,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Send the session request long timestamp = new Date().getTime(); apiDB.setSessionRequestSentTimestamp(publicKey, timestamp); - PushSessionRequestMessageSendJob job = new PushSessionRequestMessageSendJob(publicKey, timestamp); + SessionRequestMessageSendJob job = new SessionRequestMessageSendJob(publicKey, timestamp); jobManager.add(job); } // endregion diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java index c4aa802414..353dcd2149 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.UpdateApkJob; -import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import java.util.HashMap; import java.util.Map; @@ -75,7 +75,7 @@ public class WorkManagerFactoryMappings { put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY); put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY); put(PushTextSendJob.class.getName(), PushTextSendJob.KEY); - put(PushNullMessageSendJob.class.getName(), PushNullMessageSendJob.KEY); + put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY); put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY); put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY); put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index aa628dc7cc..66bf564ad9 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -14,8 +14,8 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; -import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob; -import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; import java.util.Arrays; import java.util.HashMap; @@ -51,7 +51,7 @@ public final class JobManagerFactories { put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory()); put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); - put(PushNullMessageSendJob.KEY, new PushNullMessageSendJob.Factory()); + put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory()); @@ -72,7 +72,7 @@ public final class JobManagerFactories { put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); - put(PushSessionRequestMessageSendJob.KEY, new PushSessionRequestMessageSendJob.Factory()); + put(SessionRequestMessageSendJob.KEY, new SessionRequestMessageSendJob.Factory()); put(MultiDeviceOpenGroupUpdateJob.KEY, new MultiDeviceOpenGroupUpdateJob.Factory()); }}; } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt similarity index 89% rename from src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt index 90b8ef0e00..821a281eb2 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt @@ -18,7 +18,7 @@ import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit -class PushNullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) { +class NullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) { companion object { const val KEY = "PushNullMessageSendJob" @@ -70,12 +70,12 @@ class PushNullMessageSendJob private constructor(parameters: Parameters, private override fun onCanceled() { } - class Factory : Job.Factory { + class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PushNullMessageSendJob { + override fun create(parameters: Parameters, data: Data): NullMessageSendJob { try { val publicKey = data.getString("publicKey") - return PushNullMessageSendJob(parameters, publicKey) + return NullMessageSendJob(parameters, publicKey) } catch (e: IOException) { throw AssertionError(e) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt index 4bd08e275f..fe9e07344c 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt @@ -76,7 +76,7 @@ object SessionManagementProtocol { val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID) lokiPreKeyBundleDatabase.setPreKeyBundle(publicKey, preKeyBundle) DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, Date().time) - val job = PushNullMessageSendJob(publicKey) + val job = NullMessageSendJob(publicKey) ApplicationContext.getInstance(context).jobManager.add(job) } @@ -89,7 +89,7 @@ object SessionManagementProtocol { sessionStore.archiveAllSessions(content.sender) lokiThreadDB.setSessionResetStatus(content.sender, SessionResetStatus.REQUEST_RECEIVED) Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.") - val job = PushNullMessageSendJob(content.sender) + val job = NullMessageSendJob(content.sender) ApplicationContext.getInstance(context).jobManager.add(job) SecurityEvent.broadcastSecurityUpdateEvent(context) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt similarity index 92% rename from src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt index 01b2668f71..155ca1e29b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt @@ -19,7 +19,7 @@ import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit -class PushSessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) { +class SessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) { companion object { const val KEY = "PushSessionRequestMessageSendJob" @@ -92,13 +92,13 @@ class PushSessionRequestMessageSendJob private constructor(parameters: Parameter } } - class Factory : Job.Factory { + class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PushSessionRequestMessageSendJob { + override fun create(parameters: Parameters, data: Data): SessionRequestMessageSendJob { try { val publicKey = data.getString("publicKey") val timestamp = data.getLong("timestamp") - return PushSessionRequestMessageSendJob(parameters, publicKey, timestamp) + return SessionRequestMessageSendJob(parameters, publicKey, timestamp) } catch (e: IOException) { throw AssertionError(e) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt index 4c34d64b64..1c8a283125 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt @@ -19,7 +19,7 @@ class SessionResetImplementation(private val context: Context) : SessionResetPro override fun onNewSessionAdopted(publicKey: String, oldSessionResetStatus: SessionResetStatus) { if (oldSessionResetStatus == SessionResetStatus.IN_PROGRESS) { - val job = PushNullMessageSendJob(publicKey) + val job = NullMessageSendJob(publicKey) ApplicationContext.getInstance(context).jobManager.add(job) } // TODO: Show session reset succeed message From 6be509c6574197970fb468d0f63a9874c76a3b6b Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 12:04:23 +1000 Subject: [PATCH 06/38] Implement ClosedGroupUpdateMessageSendJob --- .../migration/WorkManagerFactoryMappings.java | 2 + .../securesms/jobs/JobManagerFactories.java | 4 +- .../loki/database/SharedSenderKeysDatabase.kt | 7 +- .../ClosedGroupUpdateMessageSendJob.kt | 183 ++++++++++++++++++ 4 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java index 353dcd2149..04bb55eca4 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.UpdateApkJob; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import java.util.HashMap; @@ -76,6 +77,7 @@ public class WorkManagerFactoryMappings { put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY); put(PushTextSendJob.class.getName(), PushTextSendJob.KEY); put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY); + put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY); put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY); put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY); put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 66bf564ad9..41b31fd332 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; @@ -51,7 +52,8 @@ public final class JobManagerFactories { put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory()); put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); - put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory()); diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 749f4ba02f..1b39068b49 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -22,7 +22,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : private val keyIndex = "key_index" private val messageKeys = "message_keys" @JvmStatic val createClosedGroupRatchetsTableCommand - = "CREATE TABLE $closedGroupRatchetsTable (PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey), $chainKey STRING, $keyIndex INTEGER DEFAULT 0, $messageKeys STRING);" + = "CREATE TABLE $closedGroupRatchetsTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + + "$keyIndex INTEGER DEFAULT 0, $messageKeys STRING, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" // Private keys private val closedGroupPrivateKeysTable = "closed_group_private_keys" private val closedGroupPrivateKey = "closed_group_private_key" @@ -37,7 +38,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : return database.get(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) - val messageKeys = cursor.getString(Companion.messageKeys).split("-") + val messageKeys = cursor.getString(Companion.messageKeys).split(" - ") ClosedGroupRatchet(chainKey, keyIndex, messageKeys) } } @@ -49,7 +50,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : values.put(Companion.senderPublicKey, senderPublicKey) values.put(Companion.chainKey, ratchet.chainKey) values.put(Companion.keyIndex, ratchet.keyIndex) - values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-")) + values.put(Companion.messageKeys, ratchet.messageKeys.joinToString(" - ")) val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" database.insertOrUpdate(closedGroupRatchetsTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt new file mode 100644 index 0000000000..f45f701d17 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -0,0 +1,183 @@ +package org.thoughtcrime.securesms.loki.protocol + +import com.google.protobuf.ByteString +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.BaseJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.utilities.recipient +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities +import org.whispersystems.signalservice.loki.utilities.toHexString +import java.io.IOException +import java.security.SecureRandom +import java.util.* +import java.util.concurrent.TimeUnit + +class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { + + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: ClosedGroupSenderKey) : Kind() + } + + companion object { + const val KEY = "ClosedGroupUpdateMessageSendJob" + } + + constructor(destination: String, kind: Kind) : this(Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue(KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(1) + .build(), + destination, + kind) + + override fun getFactoryKey(): String { return KEY } + + override fun serialize(): Data { + val builder = Data.Builder() + builder.putString("destination", destination) + when (kind) { + is Kind.New -> { + builder.putString("kind", "New") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + builder.putByteArray("groupPrivateKey", kind.groupPrivateKey) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.Info -> { + builder.putString("kind", "Info") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.SenderKeyRequest -> { + builder.putString("kind", "SenderKeyRequest") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + } + is Kind.SenderKey -> { + builder.putString("kind", "SenderKey") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("senderKey", kind.senderKey.toJSON()) + } + } + return builder.build() + } + + public override fun onRun() { + val contentMessage = SignalServiceProtos.Content.newBuilder() + val dataMessage = SignalServiceProtos.DataMessage.newBuilder() + val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdate.newBuilder() + when (kind) { + is Kind.New -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey) + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.Info -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.SenderKeyRequest -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + } + is Kind.SenderKey -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() )) + } + } + dataMessage.closedGroupUpdate = closedGroupUpdate.build() + contentMessage.dataMessage = dataMessage.build() + val serializedContentMessage = contentMessage.build().toByteArray() + val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() + val address = SignalServiceAddress(destination) + val recipient = recipient(context, destination) + val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) + val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) + try { + // TODO: useFallbackEncryption + // TODO: isClosedGroup + messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, + Date().time, serializedContentMessage, false, ttl, false, + false, false, false) + } catch (e: Exception) { + Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") + throw e + } + } + + public override fun onShouldRetry(e: Exception): Boolean { + // Disable since we have our own retrying + return false + } + + override fun onCanceled() { } + + class Factory : Job.Factory { + + override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJob { + val destination = data.getString("destination") + val rawKind = data.getString("kind") + val groupPublicKey = data.getByteArray("groupPublicKey") + val kind: Kind + when (rawKind) { + "New" -> { + val name = data.getString("name") + val groupPrivateKey = data.getByteArray("groupPrivateKey") + val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins) + } + "Info" -> { + val name = data.getString("name") + val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.Info(groupPublicKey, name, senderKeys, members, admins) + } + "SenderKeyRequest" -> { + kind = Kind.SenderKeyRequest(groupPublicKey) + } + "SenderKey" -> { + val senderKey = ClosedGroupSenderKey.fromJSON(data.getString("senderKey"))!! + kind = Kind.SenderKey(groupPublicKey, senderKey) + } + else -> throw Exception("Invalid closed group update message kind: $rawKind.") + } + return ClosedGroupUpdateMessageSendJob(parameters, destination, kind) + } + } +} From 7eb59c1400b7760abfdee6b3e990263e213a4cbe Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 13:47:40 +1000 Subject: [PATCH 07/38] Implement SSK group member adding Also finish group creation --- .../loki/protocol/ClosedGroupsProtocol.kt | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 522bbce2f5..3618ebebfb 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context +import android.util.Log import nl.komponents.kovenant.Promise import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address @@ -34,7 +35,7 @@ object ClosedGroupsProtocol { ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) } // Create the group - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false); + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val admins = setOf( Address.fromSerialized(userPublicKey) ) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), null, null, LinkedList
(admins)) @@ -42,15 +43,70 @@ object ClosedGroupsProtocol { // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) // Send a closed group update message to all members using established channels - // TODO + val adminsAsData = admins.map { Hex.fromStringCondensed(it.serialize()) } + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), + senderKeys, membersAsData, adminsAsData) + for (member in members) { + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // TODO: Wait for the messages to finish sending // Add the group to the user's set of public keys to poll for DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the user - // TODO + // TODO: Implement // Return return Promise.of(Unit) } + public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't add users to nonexistent closed group.") + return + } + val name = group.title + val admins = group.admins.map { Hex.fromStringCondensed(it.serialize()) } + val privateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + if (privateKey == null) { + Log.d("Loki", "Couldn't get private key for closed group.") + return + } + // Add the members to the member list + val members = group.members.map { it.serialize() }.toMutableSet() + members.addAll(newMembers) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Generate ratchets for the new members + val senderKeys: List = newMembers.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + senderKeys, membersAsData, admins) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, newMembers) + // Send closed group update messages to the new members using established channels + for (member in members) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + senderKeys, membersAsData, admins) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // Update the group + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + // TODO: Implement + } + @JvmStatic fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { if (!address.isClosedGroup || groupID == null) { return false } From 8931d904e580e3dd95e246aaed56b99f9e83a870 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 15:41:53 +1000 Subject: [PATCH 08/38] Implement SSK group member removing --- .../loki/protocol/ClosedGroupsProtocol.kt | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 3618ebebfb..765da2c326 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -61,7 +61,6 @@ object ClosedGroupsProtocol { public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() @@ -107,6 +106,61 @@ object ClosedGroupsProtocol { // TODO: Implement } + public fun leave(context: Context, groupPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + removeMembers(context, setOf( userPublicKey ), groupPublicKey) + } + + public fun removeMembers(context: Context, membersToRemove: Collection, groupPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + val isUserLeaving = membersToRemove.contains(userPublicKey) + if (isUserLeaving && membersToRemove.count() != 1) { + Log.d("Loki", "Can't remove self and others simultaneously.") + } + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't add users to nonexistent closed group.") + return + } + val name = group.title + val admins = group.admins.map { Hex.fromStringCondensed(it.serialize()) } + // Remove the members from the member list + val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), + name, setOf(), membersAsData, admins) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + // Delete all ratchets (it's important that this happens after sending out the update) + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if (isUserLeaving) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + } else { + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } + // Update the group + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + // TODO: Implement + } + @JvmStatic fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { if (!address.isClosedGroup || groupID == null) { return false } From 13dd8dd250a9412e4bfce4a22cb877146ce4e369 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 16:30:41 +1000 Subject: [PATCH 09/38] Finish ClosedGroupsProtocol --- .../loki/protocol/ClosedGroupsProtocol.kt | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 765da2c326..db52522d69 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -14,10 +14,13 @@ import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey +import org.whispersystems.signalservice.loki.utilities.toHexString import java.util.* object ClosedGroupsProtocol { @@ -161,6 +164,156 @@ object ClosedGroupsProtocol { // TODO: Implement } + public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + when (closedGroupUpdate.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate) + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> handleSenderKeyRequest(context, closedGroupUpdate) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> handleSenderKey(context, closedGroupUpdate) + else -> { + // Do nothing + } + } + } + + public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + // Unwrap the message + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val name = closedGroupUpdate.name + val groupPrivateKey = closedGroupUpdate.groupPrivateKey.toByteArray() + val senderKeys = closedGroupUpdate.senderKeysList.map { + ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray()) + } + val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } + // Persist the ratchets + senderKeys.forEach { senderKey -> + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) + } + // Create the group + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Add the group to the user's set of public keys to poll for + sskDatabase.setClosedGroupPrivateKey(groupPrivateKey.toHexString(), groupPublicKey) + // Notify the user + // TODO: Implement + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + } + + public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + // Unwrap the message + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val name = closedGroupUpdate.name + val senderKeys = closedGroupUpdate.senderKeysList.map { + ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray()) + } + val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group update for nonexistent group.") + return + } + val oldMembers = group.members.map { it.serialize() } + val senderPublicKey = "" // TODO + // Check that the sender is a member of the group (before the update) + if (!oldMembers.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring closed group info message from non-member.") + } + // Store the ratchets for any new members (it's important that this happens before the code below) + senderKeys.forEach { senderKey -> + // TODO: Ignore sender keys if the public key they specify isn't a member of the group + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) + } + // 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 wasUserRemoved = !members.contains(userPublicKey) + if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) { + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) + if (wasUserRemoved) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + } else { + establishSessionsWithMembersIfNeeded(context, members) + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } + } + // Update the group + groupDB.updateTitle(groupID, name) + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + // TODO: Implement + } + + public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring sender key request for nonexistent group.") + return + } + // Check that the requesting user is a member of the group + val senderPublicKey = "" + if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring sender key request from non-member.") + } + // Respond to the request + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + + public fun handleSenderKey(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group sender key for nonexistent group.") + return + } + val senderKeyProto = closedGroupUpdate.senderKeysList.firstOrNull() + if (senderKeyProto == null) { + Log.d("Loki", "Ignoring invalid closed group sender key.") + return + } + val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray()) + val senderPublicKey = senderKeyProto.publicKey.toByteArray().toHexString() + // Check that the sending user is a member of the group + if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring closed group sender key from non-member.") + } + // Store the sender key + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet) + } + @JvmStatic fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { if (!address.isClosedGroup || groupID == null) { return false } From 1e16a940fe2bd697fc9c25b672fe2941c0d37e5e Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 09:23:51 +1000 Subject: [PATCH 10/38] Clean & debug --- .../loki/protocol/ClosedGroupsProtocol.kt | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index db52522d69..f44250b994 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender -import org.thoughtcrime.securesms.sms.OutgoingTextMessage import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -39,14 +38,14 @@ object ClosedGroupsProtocol { } // Create the group val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) - val admins = setOf( Address.fromSerialized(userPublicKey) ) + val admins = setOf( userPublicKey ) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), - null, null, LinkedList
(admins)) + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) // Send a closed group update message to all members using established channels - val adminsAsData = admins.map { Hex.fromStringCondensed(it.serialize()) } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), senderKeys, membersAsData, adminsAsData) for (member in members) { @@ -64,6 +63,7 @@ object ClosedGroupsProtocol { public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() @@ -73,8 +73,8 @@ object ClosedGroupsProtocol { } val name = group.title val admins = group.admins.map { Hex.fromStringCondensed(it.serialize()) } - val privateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) - if (privateKey == null) { + val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + if (groupPrivateKey == null) { Log.d("Loki", "Couldn't get private key for closed group.") return } @@ -95,10 +95,11 @@ object ClosedGroupsProtocol { // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, newMembers) // Send closed group update messages to the new members using established channels + val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys for (member in members) { @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - senderKeys, membersAsData, admins) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, admins) @Suppress("NAME_SHADOWING") val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) @@ -120,6 +121,7 @@ object ClosedGroupsProtocol { val isUserLeaving = membersToRemove.contains(userPublicKey) if (isUserLeaving && membersToRemove.count() != 1) { Log.d("Loki", "Can't remove self and others simultaneously.") + return } val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) @@ -164,12 +166,21 @@ object ClosedGroupsProtocol { // TODO: Implement } - public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { + // Establish session if needed + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) + // Send the request + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) + val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + + public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate) - SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate) - SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> handleSenderKeyRequest(context, closedGroupUpdate) - SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> handleSenderKey(context, closedGroupUpdate) + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> handleSenderKeyRequest(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> handleSenderKey(context, closedGroupUpdate, senderPublicKey) else -> { // Do nothing } @@ -190,6 +201,7 @@ object ClosedGroupsProtocol { val admins = closedGroupUpdate.adminsList.map { it.toByteArray().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) } @@ -206,7 +218,7 @@ object ClosedGroupsProtocol { establishSessionsWithMembersIfNeeded(context, members) } - public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) val sskDatabase = DatabaseFactory.getSSKDatabase(context) @@ -221,18 +233,18 @@ object ClosedGroupsProtocol { val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { - Log.d("Loki", "Ignoring closed group update for nonexistent group.") + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") return } val oldMembers = group.members.map { it.serialize() } - val senderPublicKey = "" // TODO // Check that the sender is a member of the group (before the update) if (!oldMembers.contains(senderPublicKey)) { Log.d("Loki", "Ignoring closed group info message from non-member.") + return } // Store the ratchets for any new members (it's important that this happens before the code below) senderKeys.forEach { senderKey -> - // TODO: Ignore sender keys if the public key they specify isn't a member of the group + if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) } @@ -262,7 +274,7 @@ object ClosedGroupsProtocol { // TODO: Implement } - public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() @@ -270,13 +282,13 @@ object ClosedGroupsProtocol { val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { - Log.d("Loki", "Ignoring sender key request for nonexistent group.") + Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") return } // Check that the requesting user is a member of the group - val senderPublicKey = "" if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { - Log.d("Loki", "Ignoring sender key request from non-member.") + Log.d("Loki", "Ignoring closed group sender key request from non-member.") + return } // Respond to the request ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) @@ -287,7 +299,7 @@ object ClosedGroupsProtocol { ApplicationContext.getInstance(context).jobManager.add(job) } - public fun handleSenderKey(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + public fun handleSenderKey(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { // Prepare val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() @@ -304,10 +316,14 @@ object ClosedGroupsProtocol { return } val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray()) - val senderPublicKey = senderKeyProto.publicKey.toByteArray().toHexString() // Check that the sending user is a member of the group if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { Log.d("Loki", "Ignoring closed group sender key from non-member.") + return + } + if (senderKeyProto.publicKey.toByteArray().toHexString() != senderPublicKey) { + Log.d("Loki", "Ignoring invalid closed group sender key.") + return } // Store the sender key val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) From 4ac15190bb1a13c0fb15abb54795b37e01c17438 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 09:53:37 +1000 Subject: [PATCH 11/38] Implement ClosedGroupPoller --- .../securesms/loki/api/ClosedGroupPoller.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt new file mode 100644 index 0000000000..6a3aa9fe8f --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -0,0 +1,81 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.Context +import android.os.Handler +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.thoughtcrime.securesms.jobs.PushContentReceiveJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase +import org.thoughtcrime.securesms.loki.utilities.successBackground +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope +import org.whispersystems.signalservice.loki.api.SnodeAPI +import org.whispersystems.signalservice.loki.api.SwarmAPI +import org.whispersystems.signalservice.loki.utilities.getRandomElementOrNull + +class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) { + private var isPolling = false + private val handler = Handler() + + private val task = object : Runnable { + + override fun run() { + poll() + handler.postDelayed(this, ClosedGroupPoller.pollInterval) + } + } + + // region Settings + companion object { + private val pollInterval: Long = 2 * 1000 + + public lateinit var shared: ClosedGroupPoller + + public fun configureIfNeeded(context: Context, sskDatabase: SharedSenderKeysDatabase) { + if (::shared.isInitialized) { return; } + shared = ClosedGroupPoller(context, sskDatabase) + } + } + // endregion + + // region Error + public class InsufficientSnodesException() : Exception("No snodes left to poll.") + public class PollingCanceledException() : Exception("Polling canceled.") + // endregion + + // region Public API + public fun startIfNeeded() { + if (isPolling) { return } + isPolling = true + task.run() + } + + public fun stopIfNeeded() { + isPolling = false + handler.removeCallbacks(task) + } + // endregion + + // region Private API + private fun poll() { + if (!isPolling) { return } + val publicKeys = database.getAllClosedGroupPublicKeys() + publicKeys.forEach { publicKey -> + SwarmAPI.shared.getSwarm(publicKey).bind { swarm -> + val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure + if (!isPolling) { throw PollingCanceledException() } + SnodeAPI.shared.getRawMessages(snode).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode) } + }.successBackground { messages -> + if (messages.isNotEmpty()) { + Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.") + } + messages.forEach { + PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) + } + }.fail { + Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.") + } + } + } + // endregion +} From 2c22ab70b792b39d5816359c851dcfc031abd835 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 11:40:43 +1000 Subject: [PATCH 12/38] Update database for SSKs --- .../database/helpers/SQLCipherOpenHelper.java | 52 ++-- .../loki/api/BackgroundPollWorker.kt | 2 +- .../securesms/loki/api/ClosedGroupPoller.kt | 2 +- .../loki/database/LokiAPIDatabase.kt | 272 +++++++++--------- .../loki/database/SharedSenderKeysDatabase.kt | 28 +- 5 files changed, 179 insertions(+), 177 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 14708f9f38..58afc5520d 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -136,20 +136,20 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } db.execSQL(StickerDatabase.CREATE_TABLE); - db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSwarmCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand()); @@ -159,8 +159,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); - db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetsTableCommand()); - db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeysTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -523,9 +523,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV1) { - db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); } if (oldVersion < lokiV2) { @@ -545,7 +545,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV5) { - db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); } if (oldVersion < lokiV6) { @@ -594,22 +594,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV9) { - db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); } if (oldVersion < lokiV10) { - db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); } if (oldVersion < lokiV11) { - db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); } if (oldVersion < lokiV12) { - db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetsTableCommand()); - db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeysTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt index 46ffe29abb..9cd363823e 100644 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -36,7 +36,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() { val applicationContext = context.applicationContext as ApplicationContext val broadcaster = applicationContext.broadcaster SnodeAPI.configureIfNeeded(userPublicKey, lokiAPIDatabase, broadcaster) - SnodeAPI.shared.getMessages().map { messages -> + SnodeAPI.shared.getMessages(userPublicKey).map { messages -> messages.forEach { PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) } diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt index 6a3aa9fe8f..7109640bba 100644 --- a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -64,7 +64,7 @@ class ClosedGroupPoller private constructor(private val context: Context, privat SwarmAPI.shared.getSwarm(publicKey).bind { swarm -> val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure if (!isPolling) { throw PollingCanceledException() } - SnodeAPI.shared.getRawMessages(snode).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode) } + SnodeAPI.shared.getRawMessages(snode, publicKey).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode, publicKey) } }.successBackground { messages -> if (messages.isNotEmpty()) { Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.") diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index fefcfcc211..11b14e2955 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -6,7 +6,6 @@ import android.util.Log import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.api.Snode import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol @@ -14,78 +13,73 @@ import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.Device class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { - private val userPublicKey get() = TextSecurePreferences.getLocalNumber(context) - companion object { // Shared private val publicKey = "public_key" private val timestamp = "timestamp" - // Snode pool cache - private val snodePoolCache = "loki_snode_pool_cache" + private val snode = "snode" + // Snode pool + private val snodePoolTable = "loki_snode_pool_cache" private val dummyKey = "dummy_key" private val snodePool = "snode_pool_key" - @JvmStatic val createSnodePoolCacheCommand = "CREATE TABLE $snodePoolCache ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);" - // Onion request path cache - private val onionRequestPathCache = "loki_path_cache" + @JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey STRING PRIMARY KEY, $snodePool STRING);" + // Onion request paths + private val onionRequestPathTable = "loki_path_cache" private val indexPath = "index_path" - private val snode = "snode" - @JvmStatic val createOnionRequestPathCacheCommand = "CREATE TABLE $onionRequestPathCache ($indexPath TEXT PRIMARY KEY, $snode TEXT);" - // Swarm cache - private val swarmCache = "loki_api_swarm_cache" + @JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath STRING PRIMARY KEY, $snode STRING);" + // Swarms + private val swarmTable = "loki_api_swarm_cache" private val swarmPublicKey = "hex_encoded_public_key" private val swarm = "swarm" - @JvmStatic val createSwarmCacheCommand = "CREATE TABLE $swarmCache ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);" - // Last message hash value cache - private val lastMessageHashValueCache = "loki_api_last_message_hash_value_cache" - private val target = "target" + @JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey STRING PRIMARY KEY, $swarm STRING);" + // Last message hash values + private val lastMessageHashValueTable2 = "last_message_hash_value_table" private val lastMessageHashValue = "last_message_hash_value" - @JvmStatic val createLastMessageHashValueCacheCommand = "CREATE TABLE $lastMessageHashValueCache ($target TEXT PRIMARY KEY, $lastMessageHashValue TEXT);" - // Received message hash values cache - private val receivedMessageHashValuesCache = "loki_api_received_message_hash_values_cache" - private val userID = "user_id" + @JvmStatic val createLastMessageHashValueTable2Command + = "CREATE TABLE $lastMessageHashValueTable2 ($snode STRING, $publicKey STRING, $lastMessageHashValue STRING, PRIMARY KEY ($snode, $publicKey));" + // Received message hash values + private val receivedMessageHashValuesTable2 = "received_message_hash_values_table" private val receivedMessageHashValues = "received_message_hash_values" - @JvmStatic val createReceivedMessageHashValuesCacheCommand = "CREATE TABLE $receivedMessageHashValuesCache ($userID TEXT PRIMARY KEY, $receivedMessageHashValues TEXT);" - // Open group auth token cache - private val openGroupAuthTokenCache = "loki_api_group_chat_auth_token_database" + @JvmStatic val createReceivedMessageHashValuesTable2Command + = "CREATE TABLE $receivedMessageHashValuesTable2 ($snode STRING, $publicKey STRING, $receivedMessageHashValues STRING, PRIMARY KEY ($snode, $publicKey));" + // Open group auth tokens + private val openGroupAuthTokenTable = "loki_api_group_chat_auth_token_database" private val server = "server" private val token = "token" - @JvmStatic val createOpenGroupAuthTokenCacheCommand = "CREATE TABLE $openGroupAuthTokenCache ($server TEXT PRIMARY KEY, $token TEXT);" - // Last message server ID cache - private val lastMessageServerIDCache = "loki_api_last_message_server_id_cache" - private val lastMessageServerIDCacheIndex = "loki_api_last_message_server_id_cache_index" + @JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server STRING PRIMARY KEY, $token STRING);" + // Last message server IDs + private val lastMessageServerIDTable = "loki_api_last_message_server_id_cache" + private val lastMessageServerIDTableIndex = "loki_api_last_message_server_id_cache_index" private val lastMessageServerID = "last_message_server_id" - @JvmStatic val createLastMessageServerIDCacheCommand = "CREATE TABLE $lastMessageServerIDCache ($lastMessageServerIDCacheIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);" - // Last deletion server ID cache - private val lastDeletionServerIDCache = "loki_api_last_deletion_server_id_cache" - private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index" + @JvmStatic val createLastMessageServerIDTableCommand = "CREATE TABLE $lastMessageServerIDTable ($lastMessageServerIDTableIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);" + // Last deletion server IDs + private val lastDeletionServerIDTable = "loki_api_last_deletion_server_id_cache" + private val lastDeletionServerIDTableIndex = "loki_api_last_deletion_server_id_cache_index" private val lastDeletionServerID = "last_deletion_server_id" - @JvmStatic val createLastDeletionServerIDCacheCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);" - // Device link cache + @JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDTable ($lastDeletionServerIDTableIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);" + // User counts + private val userCountTable = "loki_user_count_cache" + private val publicChatID = "public_chat_id" + private val userCount = "user_count" + @JvmStatic val createUserCountTableCommand = "CREATE TABLE $userCountTable ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);" + // Session request sent timestamps + private val sessionRequestSentTimestampTable = "session_request_sent_timestamp_cache" + @JvmStatic val createSessionRequestSentTimestampTableCommand = "CREATE TABLE $sessionRequestSentTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" + // Session request processed timestamp cache + private val sessionRequestProcessedTimestampTable = "session_request_processed_timestamp_cache" + @JvmStatic val createSessionRequestProcessedTimestampTableCommand = "CREATE TABLE $sessionRequestProcessedTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" + // Open group public keys + private val openGroupPublicKeyTable = "open_group_public_keys" + @JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);" + + // region Deprecated private val deviceLinkCache = "loki_pairing_authorisation_cache" private val masterPublicKey = "primary_device" private val slavePublicKey = "secondary_device" private val requestSignature = "request_signature" private val authorizationSignature = "grant_signature" - @JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey TEXT, $slavePublicKey TEXT, " + - "$requestSignature TEXT NULLABLE DEFAULT NULL, $authorizationSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));" - // User count cache - private val userCountCache = "loki_user_count_cache" - private val publicChatID = "public_chat_id" - private val userCount = "user_count" - @JvmStatic val createUserCountCacheCommand = "CREATE TABLE $userCountCache ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);" - // Session request sent timestamp cache - private val sessionRequestSentTimestampCache = "session_request_sent_timestamp_cache" - @JvmStatic val createSessionRequestSentTimestampCacheCommand = "CREATE TABLE $sessionRequestSentTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" - // Session request processed timestamp cache - private val sessionRequestProcessedTimestampCache = "session_request_processed_timestamp_cache" - @JvmStatic val createSessionRequestProcessedTimestampCacheCommand = "CREATE TABLE $sessionRequestProcessedTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" - // Open group public keys - private val openGroupPublicKeyDB = "open_group_public_keys" - @JvmStatic val createOpenGroupPublicKeyDBCommand = "CREATE TABLE $openGroupPublicKeyDB ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);" - - - - // region Deprecated + @JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey STRING, $slavePublicKey STRING, " + + "$requestSignature STRING NULLABLE DEFAULT NULL, $authorizationSignature STRING NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));" private val sessionRequestTimestampCache = "session_request_timestamp_cache" @JvmStatic val createSessionRequestTimestampCacheCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp STRING);" // endregion @@ -93,7 +87,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun getSnodePool(): Set { val database = databaseHelper.readableDatabase - return database.get(snodePoolCache, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> + return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool)) snodePoolAsString.split(", ").mapNotNull { snodeAsString -> val components = snodeAsString.split("-") @@ -117,13 +111,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( string } val row = wrap(mapOf(Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString)) - database.insertOrUpdate(snodePoolCache, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) + database.insertOrUpdate(snodePoolTable, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) } override fun getOnionRequestPaths(): List> { val database = databaseHelper.readableDatabase fun get(indexPath: String): Snode? { - return database.get(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> + return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode)) val components = snodeAsString.split("-") val address = components[0] @@ -146,7 +140,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( fun clearOnionRequestPaths() { val database = databaseHelper.writableDatabase fun delete(indexPath: String) { - database.delete(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath)) + database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) } delete("0-0"); delete("0-1") delete("0-2"); delete("1-0") @@ -154,7 +148,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } override fun setOnionRequestPaths(newValue: List>) { - // FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this. + // TODO: Make this work with arbitrary paths if (newValue.count() != 2) { return } val path0 = newValue[0] val path1 = newValue[1] @@ -168,7 +162,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" } val row = wrap(mapOf(Companion.indexPath to indexPath, Companion.snode to snodeAsString)) - database.insertOrUpdate(onionRequestPathCache, row, "${Companion.indexPath} = ?", wrap(indexPath)) + database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) } set("0-0", path0[0]); set("0-1", path0[1]) set("0-2", path0[2]); set("1-0", path1[0]) @@ -177,7 +171,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun getSwarm(publicKey: String): Set? { val database = databaseHelper.readableDatabase - return database.get(swarmCache, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> + return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) swarmAsString.split(", ").mapNotNull { targetAsString -> val components = targetAsString.split("-") @@ -201,40 +195,44 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( string } val row = wrap(mapOf(Companion.swarmPublicKey to publicKey, swarm to swarmAsString)) - database.insertOrUpdate(swarmCache, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) + database.insertOrUpdate(swarmTable, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) } - override fun getLastMessageHashValue(snode: Snode): String? { + override fun getLastMessageHashValue(snode: Snode, publicKey: String): String? { val database = databaseHelper.readableDatabase - return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(snode.address)) { cursor -> + val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" + return database.get(lastMessageHashValueTable2, query, arrayOf( snode.address, publicKey )) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue)) } } - override fun setLastMessageHashValue(snode: Snode, newValue: String) { + override fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String) { val database = databaseHelper.writableDatabase - val row = wrap(mapOf(Companion.target to snode.address, lastMessageHashValue to newValue)) - database.insertOrUpdate(lastMessageHashValueCache, row, "${Companion.target} = ?", wrap(snode.address)) + val row = wrap(mapOf(Companion.snode to snode.address, Companion.publicKey to publicKey, lastMessageHashValue to newValue)) + val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" + database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.address, publicKey )) } - override fun getReceivedMessageHashValues(): Set? { + override fun getReceivedMessageHashValues(publicKey: String): Set? { val database = databaseHelper.readableDatabase - return database.get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor -> + val query = "$Companion.publicKey = ?" + return database.get(receivedMessageHashValuesTable2, query, arrayOf( publicKey )) { cursor -> val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) receivedMessageHashValuesAsString.split(", ").toSet() } } - override fun setReceivedMessageHashValues(newValue: Set) { + override fun setReceivedMessageHashValues(publicKey: String, newValue: Set) { val database = databaseHelper.writableDatabase val receivedMessageHashValuesAsString = newValue.joinToString(", ") - val row = wrap(mapOf(userID to userPublicKey, receivedMessageHashValues to receivedMessageHashValuesAsString)) - database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey)) + val row = wrap(mapOf( Companion.publicKey to publicKey, receivedMessageHashValues to receivedMessageHashValuesAsString )) + val query = "$Companion.publicKey = ?" + database.insertOrUpdate(receivedMessageHashValuesTable2, row, query, arrayOf( publicKey )) } override fun getAuthToken(server: String): String? { val database = databaseHelper.readableDatabase - return database.get(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server)) { cursor -> + return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(token)) } } @@ -243,16 +241,16 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val database = databaseHelper.writableDatabase if (newValue != null) { val row = wrap(mapOf(Companion.server to server, token to newValue)) - database.insertOrUpdate(openGroupAuthTokenCache, row, "${Companion.server} = ?", wrap(server)) + database.insertOrUpdate(openGroupAuthTokenTable, row, "${Companion.server} = ?", wrap(server)) } else { - database.delete(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server)) + database.delete(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) } } override fun getLastMessageServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" - return database.get(lastMessageServerIDCache, "$lastMessageServerIDCacheIndex = ?", wrap(index)) { cursor -> + return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor -> cursor.getInt(lastMessageServerID) }?.toLong() } @@ -260,20 +258,20 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastMessageServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastMessageServerIDCacheIndex to index, lastMessageServerID to newValue.toString())) - database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index)) + val row = wrap(mapOf(lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString())) + database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index)) } fun removeLastMessageServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" - database.delete(lastMessageServerIDCache,"$lastMessageServerIDCacheIndex = ?", wrap(index)) + database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index)) } override fun getLastDeletionServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" - return database.get(lastDeletionServerIDCache, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) { cursor -> + return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor -> cursor.getInt(lastDeletionServerID) }?.toLong() } @@ -281,16 +279,71 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString())) - database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) + val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString())) + database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } fun removeLastDeletionServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" - database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index)) + database.delete(lastDeletionServerIDTable,"$lastDeletionServerIDTableIndex = ?", wrap(index)) } + fun getUserCount(group: Long, server: String): Int? { + val database = databaseHelper.readableDatabase + val index = "$server.$group" + return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor -> + cursor.getInt(userCount) + }?.toInt() + } + + override fun setUserCount(group: Long, server: String, newValue: Int) { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + val row = wrap(mapOf(publicChatID to index, Companion.userCount to newValue.toString())) + database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) + } + + override fun getSessionRequestSentTimestamp(publicKey: String): Long? { + val database = databaseHelper.readableDatabase + return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> + cursor.getInt(LokiAPIDatabase.timestamp) + }?.toLong() + } + + override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) + database.insertOrUpdate(sessionRequestSentTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) + } + + override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? { + val database = databaseHelper.readableDatabase + return database.get(sessionRequestProcessedTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> + cursor.getInt(LokiAPIDatabase.timestamp) + }?.toLong() + } + + override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) + database.insertOrUpdate(sessionRequestProcessedTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) + } + + override fun getOpenGroupPublicKey(server: String): String? { + val database = databaseHelper.readableDatabase + return database.get(openGroupPublicKeyTable, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor -> + cursor.getString(LokiAPIDatabase.publicKey) + } + } + + override fun setOpenGroupPublicKey(server: String, newValue: String) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf(LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue)) + database.insertOrUpdate(openGroupPublicKeyTable, row, "${LokiAPIDatabase.server} = ?", wrap(server)) + } + + // region Deprecated override fun getDeviceLinks(publicKey: String): Set { return setOf() /* @@ -330,60 +383,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.delete(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( deviceLink.masterPublicKey, deviceLink.slavePublicKey )) */ } - - fun getUserCount(group: Long, server: String): Int? { - val database = databaseHelper.readableDatabase - val index = "$server.$group" - return database.get(userCountCache, "$publicChatID = ?", wrap(index)) { cursor -> - cursor.getInt(userCount) - }?.toInt() - } - - override fun setUserCount(group: Long, server: String, newValue: Int) { - val database = databaseHelper.writableDatabase - val index = "$server.$group" - val row = wrap(mapOf(publicChatID to index, Companion.userCount to newValue.toString())) - database.insertOrUpdate(userCountCache, row, "$publicChatID = ?", wrap(index)) - } - - override fun getSessionRequestSentTimestamp(publicKey: String): Long? { - val database = databaseHelper.readableDatabase - return database.get(sessionRequestSentTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> - cursor.getInt(LokiAPIDatabase.timestamp) - }?.toLong() - } - - override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) - database.insertOrUpdate(sessionRequestSentTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) - } - - override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? { - val database = databaseHelper.readableDatabase - return database.get(sessionRequestProcessedTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> - cursor.getInt(LokiAPIDatabase.timestamp) - }?.toLong() - } - - override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) - database.insertOrUpdate(sessionRequestProcessedTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) - } - - override fun getOpenGroupPublicKey(server: String): String? { - val database = databaseHelper.readableDatabase - return database.get(openGroupPublicKeyDB, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor -> - cursor.getString(LokiAPIDatabase.publicKey) - } - } - - override fun setOpenGroupPublicKey(server: String, newValue: String) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue)) - database.insertOrUpdate(openGroupPublicKeyDB, row, "${LokiAPIDatabase.server} = ?", wrap(server)) - } + // endregion } // region Convenience diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 1b39068b49..dc89b68856 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -16,26 +16,26 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : // Shared private val closedGroupPublicKey = "closed_group_public_key" // Ratchets - private val closedGroupRatchetsTable = "closed_group_ratchets" + private val closedGroupRatchetTable = "closed_group_ratchet_table" private val senderPublicKey = "sender_public_key" private val chainKey = "chain_key" private val keyIndex = "key_index" private val messageKeys = "message_keys" - @JvmStatic val createClosedGroupRatchetsTableCommand - = "CREATE TABLE $closedGroupRatchetsTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + + @JvmStatic val createClosedGroupRatchetTableCommand + = "CREATE TABLE $closedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + "$keyIndex INTEGER DEFAULT 0, $messageKeys STRING, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" // Private keys - private val closedGroupPrivateKeysTable = "closed_group_private_keys" + private val closedGroupPrivateKeyTable = "closed_group_private_key_table" private val closedGroupPrivateKey = "closed_group_private_key" - @JvmStatic val createClosedGroupPrivateKeysTableCommand - = "CREATE TABLE $closedGroupPrivateKeysTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);" + @JvmStatic val createClosedGroupPrivateKeyTableCommand + = "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);" } // region Ratchets & Sender Keys override fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet? { val database = databaseHelper.readableDatabase val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" - return database.get(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + return database.get(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) val messageKeys = cursor.getString(Companion.messageKeys).split(" - ") @@ -52,18 +52,18 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : values.put(Companion.keyIndex, ratchet.keyIndex) values.put(Companion.messageKeys, ratchet.messageKeys.joinToString(" - ")) val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" - database.insertOrUpdate(closedGroupRatchetsTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) + database.insertOrUpdate(closedGroupRatchetTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) } override fun removeAllClosedGroupRatchets(groupPublicKey: String) { val database = databaseHelper.writableDatabase - database.delete(closedGroupRatchetsTable, null, null) + database.delete(closedGroupRatchetTable, null, null) } override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set { val database = databaseHelper.readableDatabase val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" - return database.getAll(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) val senderPublicKey = cursor.getString(Companion.senderPublicKey) @@ -76,7 +76,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : override fun getClosedGroupPrivateKey(groupPublicKey: String): String? { val database = databaseHelper.readableDatabase val query = "${Companion.closedGroupPublicKey} = ?" - return database.get(closedGroupPrivateKeysTable, query, arrayOf( groupPublicKey )) { cursor -> + return database.get(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) { cursor -> cursor.getString(Companion.closedGroupPrivateKey) } } @@ -87,18 +87,18 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : values.put(Companion.closedGroupPublicKey, groupPublicKey) values.put(Companion.closedGroupPrivateKey, groupPrivateKey) val query = "${Companion.closedGroupPublicKey} = ?" - database.insertOrUpdate(closedGroupPrivateKeysTable, values, query, arrayOf( groupPublicKey )) + database.insertOrUpdate(closedGroupPrivateKeyTable, values, query, arrayOf( groupPublicKey )) } override fun removeClosedGroupPrivateKey(groupPublicKey: String) { val database = databaseHelper.writableDatabase val query = "${Companion.closedGroupPublicKey} = ?" - database.delete(closedGroupPrivateKeysTable, query, arrayOf( groupPublicKey )) + database.delete(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) } override fun getAllClosedGroupPublicKeys(): Set { val database = databaseHelper.readableDatabase - return database.getAll(closedGroupPrivateKeysTable, null, null) { cursor -> + return database.getAll(closedGroupPrivateKeyTable, null, null) { cursor -> cursor.getString(Companion.closedGroupPublicKey) }.toSet() } From 09c668acb2c0da43a91a04727f8e3e7c34a91588 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 11:48:27 +1000 Subject: [PATCH 13/38] Start and stop the closed group poller as needed --- .../thoughtcrime/securesms/ApplicationContext.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 0903e3c959..f2e869378f 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -61,11 +61,13 @@ import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker; +import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller; import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager; import org.thoughtcrime.securesms.loki.api.PublicChatManager; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; 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.SessionRequestMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; @@ -154,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Loki public MessageNotifier messageNotifier = null; public Poller poller = null; + public ClosedGroupPoller closedGroupPoller = null; public PublicChatManager publicChatManager = null; private PublicChatAPI publicChatAPI = null; public Broadcaster broadcaster = null; @@ -507,16 +510,20 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } return Unit.INSTANCE; }); + SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this); + ClosedGroupPoller.Companion.configureIfNeeded(this, sskDatabase); + closedGroupPoller = ClosedGroupPoller.Companion.getShared(); } public void startPollingIfNeeded() { setUpPollingIfNeeded(); if (poller != null) { poller.startIfNeeded(); } + if (closedGroupPoller != null) { closedGroupPoller.startIfNeeded(); } } public void stopPolling() { - if (poller == null) { return; } - poller.stopIfNeeded(); + if (poller != null) { poller.stopIfNeeded(); } + if (closedGroupPoller != null) { closedGroupPoller.stopIfNeeded(); } } private void resubmitProfilePictureIfNeeded() { From 63da8023e7b845dd4087bc6e991cdc9f3c011f83 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 13:38:07 +1000 Subject: [PATCH 14/38] Partially revert database changes Fixing this is going to require a (big) migration --- .../securesms/loki/database/LokiAPIDatabase.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index 11b14e2955..e89507a3cc 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -22,16 +22,16 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( private val snodePoolTable = "loki_snode_pool_cache" private val dummyKey = "dummy_key" private val snodePool = "snode_pool_key" - @JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey STRING PRIMARY KEY, $snodePool STRING);" + @JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);" // Onion request paths private val onionRequestPathTable = "loki_path_cache" private val indexPath = "index_path" - @JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath STRING PRIMARY KEY, $snode STRING);" + @JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);" // Swarms private val swarmTable = "loki_api_swarm_cache" private val swarmPublicKey = "hex_encoded_public_key" private val swarm = "swarm" - @JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey STRING PRIMARY KEY, $swarm STRING);" + @JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);" // Last message hash values private val lastMessageHashValueTable2 = "last_message_hash_value_table" private val lastMessageHashValue = "last_message_hash_value" @@ -41,12 +41,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( private val receivedMessageHashValuesTable2 = "received_message_hash_values_table" private val receivedMessageHashValues = "received_message_hash_values" @JvmStatic val createReceivedMessageHashValuesTable2Command - = "CREATE TABLE $receivedMessageHashValuesTable2 ($snode STRING, $publicKey STRING, $receivedMessageHashValues STRING, PRIMARY KEY ($snode, $publicKey));" + = "CREATE TABLE $receivedMessageHashValuesTable2 ($snode STRING, $publicKey STRING, $receivedMessageHashValues TEXT, PRIMARY KEY ($snode, $publicKey));" // Open group auth tokens private val openGroupAuthTokenTable = "loki_api_group_chat_auth_token_database" private val server = "server" private val token = "token" - @JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server STRING PRIMARY KEY, $token STRING);" + @JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server TEXT PRIMARY KEY, $token TEXT);" // Last message server IDs private val lastMessageServerIDTable = "loki_api_last_message_server_id_cache" private val lastMessageServerIDTableIndex = "loki_api_last_message_server_id_cache_index" From 1e223c90caddc014b5a5e8cfe223c735ff410603 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 13:44:47 +1000 Subject: [PATCH 15/38] Minor refactoring --- .../securesms/loki/database/SharedSenderKeysDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index dc89b68856..7d76ec8726 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -23,7 +23,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : private val messageKeys = "message_keys" @JvmStatic val createClosedGroupRatchetTableCommand = "CREATE TABLE $closedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + - "$keyIndex INTEGER DEFAULT 0, $messageKeys STRING, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" + "$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" // Private keys private val closedGroupPrivateKeyTable = "closed_group_private_key_table" private val closedGroupPrivateKey = "closed_group_private_key" From 86837f031ab96df46a0e22733eb1a50afb9c14dc Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 15:29:36 +1000 Subject: [PATCH 16/38] Integrate SSKs into the encryption pipeline --- src/org/thoughtcrime/securesms/ApplicationContext.java | 3 ++- .../securesms/dependencies/SignalCommunicationModule.java | 1 + src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 2 +- .../thoughtcrime/securesms/loki/activities/HomeActivity.kt | 3 ++- .../securesms/loki/activities/LandingActivity.kt | 3 ++- .../securesms/loki/database/SharedSenderKeysDatabase.kt | 5 +++++ 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index f2e869378f..44964ba018 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -186,6 +186,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this); LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this); + SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this); SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this); if (userPublicKey != null) { @@ -196,7 +197,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); } MultiDeviceProtocol.Companion.configureIfNeeded(apiDB); - SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, threadDB, this); + SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this); setUpP2PAPIIfNeeded(); PushNotificationAcknowledgement.Companion.configureIfNeeded(BuildConfig.DEBUG); if (setUpStorageAPIIfNeeded()) { diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index bb62e86933..520761b536 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -153,6 +153,7 @@ public class SignalCommunicationModule { Optional.of(new MessageSenderEventListener(context)), TextSecurePreferences.getLocalNumber(context), DatabaseFactory.getLokiAPIDatabase(context), + DatabaseFactory.getSSKDatabase(context), DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiMessageDatabase(context), DatabaseFactory.getLokiPreKeyBundleDatabase(context), diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 85c9e6e48f..c8286dfb6f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -255,7 +255,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context); SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context)); - LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); + LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); SignalServiceContent content = cipher.decrypt(envelope); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 1e55c52e8c..e4053be162 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -154,6 +154,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val apiDB = DatabaseFactory.getLokiAPIDatabase(this) val threadDB = DatabaseFactory.getLokiThreadDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this) + val sskDatabase = DatabaseFactory.getSSKDatabase(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this) val sessionResetImpl = SessionResetImplementation(this) if (userPublicKey != null) { @@ -162,7 +163,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey) application.publicChatManager.startPollersIfNeeded() } - SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application) + SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application) MultiDeviceProtocol.configureIfNeeded(apiDB) IP2Country.configureIfNeeded(this) // Preload device links to make message sending quicker diff --git a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt index 9ee7bf81c2..d19a0002ab 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt @@ -108,12 +108,13 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega val apiDB = DatabaseFactory.getLokiAPIDatabase(this) val threadDB = DatabaseFactory.getLokiThreadDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this) + val sskDatabase = DatabaseFactory.getSSKDatabase(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this) val sessionResetImpl = SessionResetImplementation(this) MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB) SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey) org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol.configureIfNeeded(apiDB) - SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application) + SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application) SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey) application.setUpP2PAPIIfNeeded() application.setUpStorageAPIIfNeeded() diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 7d76ec8726..d251208f66 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -103,4 +103,9 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : }.toSet() } // endregion + + override fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean { + return getAllClosedGroupPublicKeys().contains(groupPublicKey) + } + // endregion } From 97b35d769a934f6de47607dca504d0cfef7f916e Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 16:15:42 +1000 Subject: [PATCH 17/38] Integrate SSKs into the decryption pipeline --- src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index c8286dfb6f..425ffaf441 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -127,6 +127,7 @@ import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation; +import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; @@ -369,6 +370,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Log.w(TAG, e); } catch (SelfSendException e) { Log.i(TAG, "Dropping UD message from self."); + } catch (IOException e) { + Log.i(TAG, "IOException during message decryption."); } } From 61050444f9d171676e28f5f80ef1f800dbc10d20 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 16:38:40 +1000 Subject: [PATCH 18/38] Switch new closed group activity over to SSKs --- .../activities/CreateClosedGroupActivity.kt | 27 +++++++++++++++++++ .../loki/protocol/ClosedGroupsProtocol.kt | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index f0989dc29b..87f7020a87 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -101,6 +102,32 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC } private fun createClosedGroup() { + if (ClosedGroupsProtocol.isSharedSenderKeysEnabled) { + createSSKBasedClosedGroup() + } else { + createLegacyClosedGroup() + } + } + + private fun createSSKBasedClosedGroup() { + val name = nameEditText.text.trim() + if (name.isEmpty()) { + return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() + } + if (name.length >= 64) { + return Toast.makeText(this, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show() + } + val selectedMembers = this.selectedMembers + if (selectedMembers.count() < 2) { + return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() + } + if (selectedMembers.count() > 49) { // Minus one because we're going to include self later + return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() + } + ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers) + } + + private fun createLegacyClosedGroup() { val name = nameEditText.text.trim() if (name.isEmpty()) { return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index f44250b994..726dfa53e7 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -23,7 +23,8 @@ import org.whispersystems.signalservice.loki.utilities.toHexString import java.util.* object ClosedGroupsProtocol { - + val isSharedSenderKeysEnabled = true + public fun createClosedGroup(context: Context, name: String, members: Collection): Promise { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) From 8e14c7abb6fd1208c31c0e8f741d4e5f796428bc Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 10 Aug 2020 16:54:59 +1000 Subject: [PATCH 19/38] Debug --- res/layout/activity_create_closed_group.xml | 16 ++-------------- .../securesms/ApplicationContext.java | 2 ++ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml index 17c92d9eb2..e0e5cc43ab 100644 --- a/res/layout/activity_create_closed_group.xml +++ b/res/layout/activity_create_closed_group.xml @@ -12,27 +12,15 @@ android:orientation="vertical"> - - + android:hint="@string/activity_create_closed_group_edit_text_hint" /> Date: Tue, 11 Aug 2020 09:59:07 +1000 Subject: [PATCH 20/38] Debug message sending --- .../loki/activities/CreateClosedGroupActivity.kt | 3 ++- .../loki/protocol/ClosedGroupsProtocol.kt | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index 87f7020a87..d79a10fc0d 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -124,7 +124,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC if (selectedMembers.count() > 49) { // Minus one because we're going to include self later return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } - ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers) + val userPublicKey = TextSecurePreferences.getLocalNumber(this) + ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) } private fun createLegacyClosedGroup() { diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 726dfa53e7..bebdce37af 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -6,7 +6,9 @@ import nl.komponents.kovenant.Promise import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.utilities.recipient +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.GroupUtil @@ -57,7 +59,9 @@ object ClosedGroupsProtocol { // Add the group to the user's set of public keys to poll for DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the user - // TODO: Implement + val infoMessage = OutgoingMediaMessage(Recipient.from(context, Address.fromSerialized(groupID), false), "Test", listOf(), System.currentTimeMillis(), + 0, 0, ThreadDatabase.DistributionTypes.CONVERSATION, null, listOf(), listOf(), listOf(), listOf()) + MessageSender.send(context, infoMessage, -1, false, null) // Return return Promise.of(Unit) } @@ -349,8 +353,12 @@ object ClosedGroupsProtocol { if (GroupUtil.isOpenGroup(groupID)) { return listOf( Address.fromSerialized(groupID) ) } else { - // TODO: Shared sender keys - return DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address } + val groupPublicKey = GroupUtil.getDecodedId(groupID).toHexString() + if (DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)) { + return listOf( Address.fromSerialized(groupPublicKey) ) + } else { + return DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address } + } /* return FileServerAPI.shared.getDeviceLinks(members.map { it.address.serialize() }.toSet()).map { val result = members.flatMap { member -> From e2ce43c3cdbcecd9021245c0289efbc8c280bc6d Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 11:51:51 +1000 Subject: [PATCH 21/38] Debug --- src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 4 ++++ src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java | 3 ++- src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt | 2 +- .../securesms/loki/database/SharedSenderKeysDatabase.kt | 2 ++ .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 425ffaf441..47344b0f2f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -279,6 +279,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType { MultiDeviceProtocol.handleUnlinkingRequestIfNeeded(context, content); } else { + if (message.getClosedGroupUpdate().isPresent()) { + ClosedGroupsProtocol.handleSharedSenderKeysUpdate(context, message.getClosedGroupUpdate().get(), content.getSender()); + } + if (message.isEndSession()) { handleEndSessionMessage(content, smsMessageId); } else if (message.isGroupUpdate()) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java index 63fc0f65d5..34570b169d 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java @@ -36,7 +36,8 @@ public abstract class PushReceivedJob extends BaseJob { if (envelope.isReceipt()) { handleReceipt(envelope); - } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFallbackMessage()) { + } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() + || envelope.isUnidentifiedSender() || envelope.isFallbackMessage() || envelope.isClosedGroupCiphertext()) { handleMessage(envelope, isPushNotification); } else { Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt index 7109640bba..f3a72a474b 100644 --- a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -59,7 +59,7 @@ class ClosedGroupPoller private constructor(private val context: Context, privat // region Private API private fun poll() { if (!isPolling) { return } - val publicKeys = database.getAllClosedGroupPublicKeys() + val publicKeys = database.getAllClosedGroupPublicKeys().map { "05$it" } publicKeys.forEach { publicKey -> SwarmAPI.shared.getSwarm(publicKey).bind { swarm -> val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index d251208f66..de9e86e275 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.util.Hex import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol +import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol { @@ -105,6 +106,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : // endregion override fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean { + if (!PublicKeyValidation.isValid(groupPublicKey)) { return false } return getAllClosedGroupPublicKeys().contains(groupPublicKey) } // endregion diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index bebdce37af..50aeffeb8d 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -180,6 +180,7 @@ object ClosedGroupsProtocol { ApplicationContext.getInstance(context).jobManager.add(job) } + @JvmStatic public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate) From 6c5034f4b3db5b32e49a913f8c2758ba167476d6 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 12:20:17 +1000 Subject: [PATCH 22/38] Make group leaving use the SSK API --- .../conversation/ConversationActivity.java | 19 +++++++++++++++---- .../securesms/loki/activities/HomeActivity.kt | 8 +++++++- .../loki/protocol/ClosedGroupsProtocol.kt | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 34aeb1c16b..abb47dcb83 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -209,6 +209,7 @@ import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.ExpirationUtil; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -227,6 +228,7 @@ import org.whispersystems.signalservice.loki.protocol.mentions.Mention; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol; import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol; +import org.whispersystems.signalservice.loki.utilities.HexEncodingKt; import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation; import java.io.IOException; @@ -1165,10 +1167,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setCancelable(true); builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)); builder.setPositiveButton(R.string.yes, (dialog, which) -> { - Recipient groupRecipient = getRecipient(); - if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { - initializeEnabledCheck(); - } else { + Recipient groupRecipient = getRecipient(); + try { + String groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(groupRecipient.getAddress().toString())); + boolean isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); + if (isSSKBasedClosedGroup) { + ClosedGroupsProtocol.leave(this, groupPublicKey); + initializeEnabledCheck(); + } else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { + initializeEnabledCheck(); + } else { + Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); + } + } catch (Exception e) { Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); } }); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index e4053be162..0b03d16933 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI @@ -49,6 +50,7 @@ import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol +import org.whispersystems.signalservice.loki.utilities.toHexString class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { private lateinit var glide: GlideRequests @@ -333,7 +335,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { - if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) { + val groupPublicKey = GroupUtil.getDecodedId(recipient.address.toString()).toHexString() + val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey) + if (isSSKBasedClosedGroup) { + ClosedGroupsProtocol.leave(this, groupPublicKey) + } else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) { Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() return@setPositiveButton } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 50aeffeb8d..735770a231 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -115,6 +115,7 @@ object ClosedGroupsProtocol { // TODO: Implement } + @JvmStatic public fun leave(context: Context, groupPublicKey: String) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) removeMembers(context, setOf( userPublicKey ), groupPublicKey) From 96f235423d4cbe62f8ae0f78e9fe7efe646a35ed Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 13:31:21 +1000 Subject: [PATCH 23/38] Debug --- .../thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt | 2 +- .../thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt | 2 +- .../securesms/loki/database/SharedSenderKeysDatabase.kt | 2 ++ .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 6 +++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt index f3a72a474b..7109640bba 100644 --- a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -59,7 +59,7 @@ class ClosedGroupPoller private constructor(private val context: Context, privat // region Private API private fun poll() { if (!isPolling) { return } - val publicKeys = database.getAllClosedGroupPublicKeys().map { "05$it" } + val publicKeys = database.getAllClosedGroupPublicKeys() publicKeys.forEach { publicKey -> SwarmAPI.shared.getSwarm(publicKey).bind { swarm -> val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index e89507a3cc..732ce550f0 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -36,7 +36,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( private val lastMessageHashValueTable2 = "last_message_hash_value_table" private val lastMessageHashValue = "last_message_hash_value" @JvmStatic val createLastMessageHashValueTable2Command - = "CREATE TABLE $lastMessageHashValueTable2 ($snode STRING, $publicKey STRING, $lastMessageHashValue STRING, PRIMARY KEY ($snode, $publicKey));" + = "CREATE TABLE $lastMessageHashValueTable2 ($snode STRING, $publicKey STRING, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));" // Received message hash values private val receivedMessageHashValuesTable2 = "received_message_hash_values_table" private val receivedMessageHashValues = "received_message_hash_values" diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index de9e86e275..1b668f532e 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -101,6 +101,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : val database = databaseHelper.readableDatabase return database.getAll(closedGroupPrivateKeyTable, null, null) { cursor -> cursor.getString(Companion.closedGroupPublicKey) + }.filter { + PublicKeyValidation.isValid(it) }.toSet() } // endregion diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 735770a231..77eb4d937d 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -206,6 +206,10 @@ object ClosedGroupsProtocol { } val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } + if (groupPublicKey.isEmpty() || name.isEmpty() || groupPrivateKey.isEmpty() || senderKeys.isEmpty() || members.isEmpty() || admins.isEmpty()) { + Log.d("Loki", "Ignoring invalid new closed group.") + return + } // Persist the ratchets senderKeys.forEach { senderKey -> if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } @@ -218,7 +222,7 @@ object ClosedGroupsProtocol { null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Add the group to the user's set of public keys to poll for - sskDatabase.setClosedGroupPrivateKey(groupPrivateKey.toHexString(), groupPublicKey) + sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) // Notify the user // TODO: Implement // Establish sessions if needed From 8a86b93dd198a894018cba5933ddb572808bcfeb Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 15:05:13 +1000 Subject: [PATCH 24/38] Validate closed group update messages --- .../loki/protocol/ClosedGroupsProtocol.kt | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 77eb4d937d..784630c705 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log +import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address @@ -40,7 +41,7 @@ object ClosedGroupsProtocol { ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) } // Create the group - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val admins = setOf( userPublicKey ) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) @@ -183,6 +184,7 @@ object ClosedGroupsProtocol { @JvmStatic public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { + if (!isValid(closedGroupUpdate)) { return; } when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate) SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate, senderPublicKey) @@ -194,6 +196,22 @@ object ClosedGroupsProtocol { } } + private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate): Boolean { + if (closedGroupUpdate.groupPublicKey.isEmpty) { return false } + when (closedGroupUpdate.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { + return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.groupPrivateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty + && closedGroupUpdate.senderKeysCount > 0 && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 + } + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> { + return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> return true + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> return closedGroupUpdate.senderKeysCount > 0 + else -> return false + } + } + public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { // Prepare val sskDatabase = DatabaseFactory.getSSKDatabase(context) @@ -206,10 +224,6 @@ object ClosedGroupsProtocol { } val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } - if (groupPublicKey.isEmpty() || name.isEmpty() || groupPrivateKey.isEmpty() || senderKeys.isEmpty() || members.isEmpty() || admins.isEmpty()) { - Log.d("Loki", "Ignoring invalid new closed group.") - return - } // Persist the ratchets senderKeys.forEach { senderKey -> if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } From d8986f6147839eb9d18b98097c5704f30585ccd2 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 18:27:23 +1000 Subject: [PATCH 25/38] Clean --- .../loki/protocol/ClosedGroupUpdateMessageSendJob.kt | 12 +++++------- .../shelved/MultiDeviceOpenGroupUpdateJob.kt | 3 +-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt index f45f701d17..7be6954fe4 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -3,22 +3,20 @@ package org.thoughtcrime.securesms.loki.protocol import com.google.protobuf.ByteString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil -import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl import org.thoughtcrime.securesms.jobmanager.Data import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.BaseJob import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.loki.utilities.recipient -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.libsignal.SignalProtocolAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities import org.whispersystems.signalservice.loki.utilities.toHexString -import java.io.IOException -import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit @@ -126,12 +124,12 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters val recipient = recipient(context, destination) val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) + val useFallbackEncryption = SignalProtocolStoreImpl(context).containsSession(SignalProtocolAddress(destination, 1)) try { - // TODO: useFallbackEncryption - // TODO: isClosedGroup + // 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, - false, false, false) + useFallbackEncryption, false, false) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") throw e diff --git a/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt index eeb7baabf6..6679594639 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt @@ -32,8 +32,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) .setMaxAttempts(Parameters.UNLIMITED) .build()) - override fun getFactoryKey(): String { return KEY - } + override fun getFactoryKey(): String { return KEY } override fun serialize(): Data { return Data.EMPTY } From c9c902218e8222ad53c9471e7f41c7101615a537 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 11 Aug 2020 19:32:10 +1000 Subject: [PATCH 26/38] Fix SSK message sending --- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 784630c705..7ea98c5fc2 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -373,7 +373,7 @@ object ClosedGroupsProtocol { if (GroupUtil.isOpenGroup(groupID)) { return listOf( Address.fromSerialized(groupID) ) } else { - val groupPublicKey = GroupUtil.getDecodedId(groupID).toHexString() + val groupPublicKey = GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)).toHexString() if (DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)) { return listOf( Address.fromSerialized(groupPublicKey) ) } else { From fcb2bbb76886051a6807e78a16fdd4e02adf301b Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 10:00:35 +1000 Subject: [PATCH 27/38] Debug --- .../conversation/ConversationActivity.java | 2 +- .../securesms/jobs/PushDecryptJob.java | 2 +- .../jobs/SendDeliveryReceiptJob.java | 3 ++ .../securesms/loki/activities/HomeActivity.kt | 2 +- .../loki/database/LokiAPIDatabase.kt | 32 +++++++++---------- .../loki/database/SharedSenderKeysDatabase.kt | 4 +-- .../loki/protocol/SessionMetaProtocol.kt | 5 +++ 7 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index abb47dcb83..83ed4ef07f 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1169,7 +1169,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setPositiveButton(R.string.yes, (dialog, which) -> { Recipient groupRecipient = getRecipient(); try { - String groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(groupRecipient.getAddress().toString())); + String groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupRecipient.getAddress().toString()))); boolean isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); if (isSSKBasedClosedGroup) { ClosedGroupsProtocol.leave(this, groupPublicKey); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 47344b0f2f..cc6445c532 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -303,7 +303,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SessionMetaProtocol.handleProfileKeyUpdate(context, content); } - if (content.isNeedsReceipt()) { + if (content.isNeedsReceipt() && SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(content.getSender()))) { handleNeedsDeliveryReceipt(content, message); } } diff --git a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java index b008edb95e..fa589cd5ac 100644 --- a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -83,6 +84,8 @@ public class SendDeliveryReceiptJob extends BaseJob implements InjectableType { Collections.singletonList(messageId), timestamp); + if (!SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(address))) { return; } + messageSender.sendReceipt(remoteAddress, UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)), receiptMessage); diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 0b03d16933..a11a84699f 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -335,7 +335,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { - val groupPublicKey = GroupUtil.getDecodedId(recipient.address.toString()).toHexString() + val groupPublicKey = GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(recipient.address.toString())).toHexString() val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey) if (isSSKBasedClosedGroup) { ClosedGroupsProtocol.leave(this, groupPublicKey) diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index 732ce550f0..58728ed199 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -36,7 +36,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( private val lastMessageHashValueTable2 = "last_message_hash_value_table" private val lastMessageHashValue = "last_message_hash_value" @JvmStatic val createLastMessageHashValueTable2Command - = "CREATE TABLE $lastMessageHashValueTable2 ($snode STRING, $publicKey STRING, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));" + = "CREATE TABLE $lastMessageHashValueTable2 ($snode TEXT, $publicKey TEXT, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));" // Received message hash values private val receivedMessageHashValuesTable2 = "received_message_hash_values_table" private val receivedMessageHashValues = "received_message_hash_values" @@ -110,7 +110,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } string } - val row = wrap(mapOf(Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString)) + val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString )) database.insertOrUpdate(snodePoolTable, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) } @@ -155,13 +155,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( if (path0.count() != 3 || path1.count() != 3) { return } Log.d("Loki", "Persisting onion request paths to database.") val database = databaseHelper.writableDatabase - fun set(indexPath: String ,snode: Snode) { + fun set(indexPath: String, snode: Snode) { var snodeAsString = "${snode.address}-${snode.port}" val keySet = snode.publicKeySet if (keySet != null) { snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" } - val row = wrap(mapOf(Companion.indexPath to indexPath, Companion.snode to snodeAsString)) + val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) } set("0-0", path0[0]); set("0-1", path0[1]) @@ -194,23 +194,23 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } string } - val row = wrap(mapOf(Companion.swarmPublicKey to publicKey, swarm to swarmAsString)) + val row = wrap(mapOf( Companion.swarmPublicKey to publicKey, swarm to swarmAsString )) database.insertOrUpdate(swarmTable, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) } override fun getLastMessageHashValue(snode: Snode, publicKey: String): String? { val database = databaseHelper.readableDatabase val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" - return database.get(lastMessageHashValueTable2, query, arrayOf( snode.address, publicKey )) { cursor -> + return database.get(lastMessageHashValueTable2, query, arrayOf( snode.toString(), publicKey )) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue)) } } override fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String) { val database = databaseHelper.writableDatabase - val row = wrap(mapOf(Companion.snode to snode.address, Companion.publicKey to publicKey, lastMessageHashValue to newValue)) + val row = wrap(mapOf( Companion.snode to snode.toString(), Companion.publicKey to publicKey, lastMessageHashValue to newValue )) val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" - database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.address, publicKey )) + database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey )) } override fun getReceivedMessageHashValues(publicKey: String): Set? { @@ -218,13 +218,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val query = "$Companion.publicKey = ?" return database.get(receivedMessageHashValuesTable2, query, arrayOf( publicKey )) { cursor -> val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) - receivedMessageHashValuesAsString.split(", ").toSet() + receivedMessageHashValuesAsString.split("-").toSet() } } override fun setReceivedMessageHashValues(publicKey: String, newValue: Set) { val database = databaseHelper.writableDatabase - val receivedMessageHashValuesAsString = newValue.joinToString(", ") + val receivedMessageHashValuesAsString = newValue.joinToString("-") val row = wrap(mapOf( Companion.publicKey to publicKey, receivedMessageHashValues to receivedMessageHashValuesAsString )) val query = "$Companion.publicKey = ?" database.insertOrUpdate(receivedMessageHashValuesTable2, row, query, arrayOf( publicKey )) @@ -240,7 +240,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setAuthToken(server: String, newValue: String?) { val database = databaseHelper.writableDatabase if (newValue != null) { - val row = wrap(mapOf(Companion.server to server, token to newValue)) + val row = wrap(mapOf( Companion.server to server, token to newValue )) database.insertOrUpdate(openGroupAuthTokenTable, row, "${Companion.server} = ?", wrap(server)) } else { database.delete(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) @@ -258,7 +258,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastMessageServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString())) + val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() )) database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index)) } @@ -279,7 +279,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString())) + val row = wrap(mapOf( lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString() )) database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } @@ -300,7 +300,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setUserCount(group: Long, server: String, newValue: Int) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(publicChatID to index, Companion.userCount to newValue.toString())) + val row = wrap(mapOf( publicChatID to index, Companion.userCount to newValue.toString() )) database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) } @@ -313,7 +313,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) { val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) + val row = wrap(mapOf( LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString() )) database.insertOrUpdate(sessionRequestSentTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) } @@ -339,7 +339,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setOpenGroupPublicKey(server: String, newValue: String) { val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue)) + val row = wrap(mapOf( LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue )) database.insertOrUpdate(openGroupPublicKeyTable, row, "${LokiAPIDatabase.server} = ?", wrap(server)) } diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 1b668f532e..23afeeaca0 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -39,7 +39,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : return database.get(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) - val messageKeys = cursor.getString(Companion.messageKeys).split(" - ") + val messageKeys = cursor.getString(Companion.messageKeys).split("-") ClosedGroupRatchet(chainKey, keyIndex, messageKeys) } } @@ -51,7 +51,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : values.put(Companion.senderPublicKey, senderPublicKey) values.put(Companion.chainKey, ratchet.chainKey) values.put(Companion.keyIndex, ratchet.keyIndex) - values.put(Companion.messageKeys, ratchet.messageKeys.joinToString(" - ")) + values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-")) val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" database.insertOrUpdate(closedGroupRatchetTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt index c0dc77f4b1..1aacde4196 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt @@ -78,6 +78,11 @@ object SessionMetaProtocol { return !recipient.address.isRSSFeed } + @JvmStatic + fun shouldSendDeliveryReceipt(address: Address): Boolean { + return !address.isGroup + } + /** * Should be invoked for the recipient's master device. */ From 7a41432433793dfc6a696181e2e414bc56a58c5f Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 11:20:45 +1000 Subject: [PATCH 28/38] Notify the user when an SSK based closed group is created or updated --- .../loki/protocol/ClosedGroupsProtocol.kt | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 7ea98c5fc2..27fe4502b6 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.utilities.recipient +import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender @@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.ecc.Curve import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation @@ -60,9 +62,8 @@ object ClosedGroupsProtocol { // Add the group to the user's set of public keys to poll for DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the user - val infoMessage = OutgoingMediaMessage(Recipient.from(context, Address.fromSerialized(groupID), false), "Test", listOf(), System.currentTimeMillis(), - 0, 0, ThreadDatabase.DistributionTypes.CONVERSATION, null, listOf(), listOf(), listOf(), listOf()) - MessageSender.send(context, infoMessage, -1, false, null) + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Return return Promise.of(Unit) } @@ -71,14 +72,15 @@ object ClosedGroupsProtocol { // Prepare val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Can't add users to nonexistent closed group.") return } val name = group.title - val admins = group.admins.map { Hex.fromStringCondensed(it.serialize()) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) if (groupPrivateKey == null) { Log.d("Loki", "Couldn't get private key for closed group.") @@ -95,7 +97,7 @@ object ClosedGroupsProtocol { } // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - senderKeys, membersAsData, admins) + senderKeys, membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) // Establish sessions if needed @@ -105,7 +107,7 @@ object ClosedGroupsProtocol { for (member in members) { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, - Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, admins) + Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) @@ -113,7 +115,8 @@ object ClosedGroupsProtocol { // Update the group groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - // TODO: Implement + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) } @JvmStatic @@ -131,20 +134,21 @@ object ClosedGroupsProtocol { return } val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Can't add users to nonexistent closed group.") return } val name = group.title - val admins = group.admins.map { Hex.fromStringCondensed(it.serialize()) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } // Remove the members from the member list val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove) val membersAsData = members.map { Hex.fromStringCondensed(it) } // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), - name, setOf(), membersAsData, admins) + name, setOf(), membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) // Delete all ratchets (it's important that this happens after sending out the update) @@ -170,7 +174,9 @@ object ClosedGroupsProtocol { // Update the group groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - // TODO: Implement + val type = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertInfoMessage(context, groupID, type, name, members, admins, threadID) } public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { @@ -231,14 +237,15 @@ object ClosedGroupsProtocol { sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) } // Create the group - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Add the group to the user's set of public keys to poll for sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) // Notify the user - // TODO: Implement + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) } @@ -254,8 +261,9 @@ object ClosedGroupsProtocol { ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray()) } val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group info message for nonexistent group.") @@ -296,7 +304,9 @@ object ClosedGroupsProtocol { groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - // TODO: Implement + val type = if (wasUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertInfoMessage(context, groupID, type, name, members, admins, threadID) } public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { @@ -304,7 +314,7 @@ object ClosedGroupsProtocol { val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") @@ -329,7 +339,7 @@ object ClosedGroupsProtocol { val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group sender key for nonexistent group.") @@ -437,4 +447,19 @@ object ClosedGroupsProtocol { ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(member) } } + + private fun insertInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, + members: Collection, admins: Collection, threadID: Long) { + val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) + val groupContextBuilder = GroupContext.newBuilder() + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)))) + .setType(type) + .setName(name) + .addAllMembers(members) + .addAllAdmins(admins) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val mmsDB = DatabaseFactory.getMmsDatabase(context) + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) + mmsDB.markAsSent(infoMessageID, true) + } } \ No newline at end of file From 158c7f13c33e027d03983152622c6e958e117847 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 11:21:41 +1000 Subject: [PATCH 29/38] Make the initial message send a bit quicker --- .../conversation/ConversationActivity.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 83ed4ef07f..7dd3365317 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2398,10 +2398,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity final long id = fragment.stageOutgoingMessage(outgoingMessage); - if (!recipient.isGroupRecipient()) { - ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); - } - new AsyncTask() { @Override protected Long doInBackground(Void... param) { @@ -2409,7 +2405,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + + if (!recipient.isGroupRecipient()) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); + } + + return result; } @Override @@ -2445,10 +2447,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity silentlySetComposeText(""); final long id = fragment.stageOutgoingMessage(message); - if (!recipient.isGroupRecipient()) { - ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); - } - new AsyncTask() { @Override protected Long doInBackground(OutgoingTextMessage... messages) { @@ -2456,7 +2454,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + long result = MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + + if (!recipient.isGroupRecipient()) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); + } + + return result; } @Override From bed1600cbbc42b5a8da402d6cb6ec90c43e219f4 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 11:56:00 +1000 Subject: [PATCH 30/38] Fix group leaving --- .../securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt | 2 +- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt index 7be6954fe4..2c0f4ef702 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -161,7 +161,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters } "Info" -> { val name = data.getString("name") - val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val senderKeys = data.getStringOrDefault("senderKeys", "").split(" - ").mapNotNull { ClosedGroupSenderKey.fromJSON(it) } // Can be empty val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } kind = Kind.Info(groupPublicKey, name, senderKeys, members, admins) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 27fe4502b6..06ac198a5b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -150,7 +150,8 @@ object ClosedGroupsProtocol { val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, setOf(), membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) + job.setContext(context) + job.onRun() // Run the job immediately // Delete all ratchets (it's important that this happens after sending out the update) sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and From 418079732b03560c153facb4c7a340ef200b339f Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 12:08:10 +1000 Subject: [PATCH 31/38] Request sender keys as needed --- .../thoughtcrime/securesms/ApplicationContext.java | 12 ++++++++++-- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 0029416a3d..5090774f31 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; 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.SessionRequestMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; @@ -109,6 +110,7 @@ import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPIDelegate; import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol; import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation; +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementationDelegate; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol; import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities; @@ -141,7 +143,8 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; * * @author Moxie Marlinspike */ -public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate, SessionManagementProtocolDelegate { +public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate, + SessionManagementProtocolDelegate, SharedSenderKeysImplementationDelegate { private static final String TAG = ApplicationContext.class.getSimpleName(); private final static int OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10 MB @@ -190,7 +193,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this); SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this); - SharedSenderKeysImplementation.Companion.configureIfNeeded(sskDatabase); + SharedSenderKeysImplementation.Companion.configureIfNeeded(sskDatabase, this); if (userPublicKey != null) { SwarmAPI.Companion.configureIfNeeded(apiDB); SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster); @@ -634,5 +637,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SessionRequestMessageSendJob job = new SessionRequestMessageSendJob(publicKey, timestamp); jobManager.add(job); } + + @Override + public void requestSenderKey(@NotNull String groupPublicKey, @NotNull String senderPublicKey) { + ClosedGroupsProtocol.requestSenderKey(this, groupPublicKey, senderPublicKey); + } // endregion } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 06ac198a5b..a6fd63758f 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -180,6 +180,7 @@ object ClosedGroupsProtocol { insertInfoMessage(context, groupID, type, name, members, admins, threadID) } + @JvmStatic public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { // Establish session if needed ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) From c88d0f0520a905d2f619b1404af1dff8b5742a9e Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 12:22:16 +1000 Subject: [PATCH 32/38] Open group after it's created --- .../loki/activities/CreateClosedGroupActivity.kt | 9 ++++++--- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index d79a10fc0d..7f6a34ee46 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -17,6 +17,7 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol @@ -125,7 +126,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } val userPublicKey = TextSecurePreferences.getLocalNumber(this) - ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) + val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) + val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) + openConversation(threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) } private fun createLegacyClosedGroup() { @@ -151,7 +154,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf( admin )).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } - private fun handleOpenConversation(threadId: Long, recipient: Recipient) { + private fun openConversation(threadId: Long, recipient: Recipient) { val intent = Intent(this, ConversationActivity::class.java) intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT) @@ -179,7 +182,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC val activity = activity.get() ?: return super.onPostExecute(result) if (result.isPresent && result.get().threadId > -1) { if (!activity.isFinishing) { - activity.handleOpenConversation(result.get().threadId, result.get().groupRecipient) + activity.openConversation(result.get().threadId, result.get().groupRecipient) } } else { super.onPostExecute(result) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index a6fd63758f..fe95a2d069 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -30,7 +30,7 @@ import java.util.* object ClosedGroupsProtocol { val isSharedSenderKeysEnabled = true - public fun createClosedGroup(context: Context, name: String, members: Collection): Promise { + public fun createClosedGroup(context: Context, name: String, members: Collection): String { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) // Generate a key pair for the group @@ -65,7 +65,7 @@ object ClosedGroupsProtocol { val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Return - return Promise.of(Unit) + return groupID } public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { From 12a6bc724d40e7e86c37fbda933a41b3f6c08f38 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 14:06:18 +1000 Subject: [PATCH 33/38] Don't send read receipts in groups --- .../conversation/ConversationActivity.java | 20 ++++++++++++++++--- .../notifications/DefaultMessageNotifier.java | 4 ++-- .../notifications/MarkReadReceiver.java | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 7dd3365317..674cc18662 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1168,9 +1168,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)); builder.setPositiveButton(R.string.yes, (dialog, which) -> { Recipient groupRecipient = getRecipient(); + String groupPublicKey; + boolean isSSKBasedClosedGroup; + try { + groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupRecipient.getAddress().toString()))); + isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); + } catch (IOException e) { + groupPublicKey = null; + isSSKBasedClosedGroup = false; + } try { - String groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupRecipient.getAddress().toString()))); - boolean isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); if (isSSKBasedClosedGroup) { ClosedGroupsProtocol.leave(this, groupPublicKey); initializeEnabledCheck(); @@ -2238,13 +2245,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void markThreadAsRead() { + Recipient recipient = this.recipient; new AsyncTask() { @Override protected Void doInBackground(Long... params) { Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); - MarkReadReceiver.process(context, messageIds); + if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { + for (MarkedMessageInfo messageInfo : messageIds) { + MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo()); + } + } else { + MarkReadReceiver.process(context, messageIds); + } return null; } diff --git a/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 660e1a4ecd..0183fdf108 100644 --- a/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -213,7 +213,7 @@ public class DefaultMessageNotifier implements MessageNotifier { } @Override - public void updateNotification(@NonNull Context context, long threadId, boolean signal) + public void updateNotification(@NonNull Context context, long threadId, boolean signal) { boolean isVisible = visibleThread == threadId; @@ -221,7 +221,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Recipient recipients = DatabaseFactory.getThreadDatabase(context) .getRecipientForThreadId(threadId); - if (isVisible) { + if (isVisible && recipients != null && SessionMetaProtocol.shouldSendReadReceipt(recipients.getAddress())) { List messageIds = threads.setRead(threadId, false); MarkReadReceiver.process(context, messageIds); } diff --git a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index ca8271a3f9..8f556026e1 100644 --- a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -105,7 +105,7 @@ public class MarkReadReceiver extends BroadcastReceiver { } } - private static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) { + public static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) { if (expirationInfo.getExpiresIn() > 0 && expirationInfo.getExpireStarted() <= 0) { ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); From bbc7acfcaf3ff9d720409447382a693af3614ad9 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 12 Aug 2020 16:07:55 +1000 Subject: [PATCH 34/38] Fix info messages --- .../loki/protocol/ClosedGroupsProtocol.kt | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index fe95a2d069..7ba42d2126 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -3,20 +3,22 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString -import nl.komponents.kovenant.Promise import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.sms.IncomingGroupMessage +import org.thoughtcrime.securesms.sms.IncomingTextMessage import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.messages.SignalServiceGroup +import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet @@ -63,7 +65,7 @@ object ClosedGroupsProtocol { DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the user val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Return return groupID } @@ -116,7 +118,7 @@ object ClosedGroupsProtocol { groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) } @JvmStatic @@ -177,7 +179,7 @@ object ClosedGroupsProtocol { // Notify the user val type = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertInfoMessage(context, groupID, type, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID) } @JvmStatic @@ -247,7 +249,7 @@ object ClosedGroupsProtocol { sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) // Notify the user val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + insertIncomingInfoMessage(context, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID) // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) } @@ -306,9 +308,10 @@ object ClosedGroupsProtocol { groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - val type = if (wasUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val type0 = if (wasUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val type1 = if (wasUserRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertInfoMessage(context, groupID, type, name, members, admins, threadID) + insertIncomingInfoMessage(context, groupID, type0, type1, name, members, admins, threadID) } public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { @@ -450,7 +453,23 @@ object ClosedGroupsProtocol { } } - private fun insertInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, + private fun insertIncomingInfoMessage(context: Context, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, name: String, + members: Collection, admins: Collection, threadID: Long) { + val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) + val groupContextBuilder = GroupContext.newBuilder() + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)))) + .setType(type0) + .setName(name) + .addAllMembers(members) + .addAllAdmins(admins) + val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)), GroupType.SIGNAL, name, members.toList(), null, admins.toList()) + val m = IncomingTextMessage(Address.fromSerialized(groupID), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true) + val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") + val smsDB = DatabaseFactory.getSmsDatabase(context) + smsDB.insertMessageInbox(infoMessage) + } + + private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long) { val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() From 2f0135a413df0e1f4958afff0f636f4099580df9 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 13 Aug 2020 08:49:17 +1000 Subject: [PATCH 35/38] Fix group ID handling --- .../conversation/ConversationActivity.java | 2 +- .../securesms/loki/activities/HomeActivity.kt | 2 +- .../loki/protocol/ClosedGroupsProtocol.kt | 23 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 674cc18662..024b9a5bcc 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1171,7 +1171,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity String groupPublicKey; boolean isSSKBasedClosedGroup; try { - groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupRecipient.getAddress().toString()))); + groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(groupRecipient.getAddress().toString())); isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); } catch (IOException e) { groupPublicKey = null; diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index a11a84699f..0b03d16933 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -335,7 +335,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { - val groupPublicKey = GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(recipient.address.toString())).toHexString() + val groupPublicKey = GroupUtil.getDecodedId(recipient.address.toString()).toHexString() val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey) if (isSSKBasedClosedGroup) { ClosedGroupsProtocol.leave(this, groupPublicKey) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 7ba42d2126..0cb029a5d4 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -45,7 +45,7 @@ object ClosedGroupsProtocol { ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) } // Create the group - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val admins = setOf( userPublicKey ) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) @@ -74,7 +74,7 @@ object ClosedGroupsProtocol { // Prepare val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Can't add users to nonexistent closed group.") @@ -136,7 +136,7 @@ object ClosedGroupsProtocol { return } val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Can't add users to nonexistent closed group.") @@ -241,7 +241,7 @@ object ClosedGroupsProtocol { sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) } // Create the group - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) @@ -267,7 +267,7 @@ object ClosedGroupsProtocol { val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group info message for nonexistent group.") @@ -319,7 +319,7 @@ object ClosedGroupsProtocol { val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") @@ -344,7 +344,7 @@ object ClosedGroupsProtocol { val sskDatabase = DatabaseFactory.getSSKDatabase(context) val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false) // Signal double encodes the group ID + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group sender key for nonexistent group.") @@ -388,7 +388,7 @@ object ClosedGroupsProtocol { if (GroupUtil.isOpenGroup(groupID)) { return listOf( Address.fromSerialized(groupID) ) } else { - val groupPublicKey = GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)).toHexString() + val groupPublicKey = GroupUtil.getDecodedId(groupID).toHexString() if (DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)) { return listOf( Address.fromSerialized(groupPublicKey) ) } else { @@ -455,14 +455,13 @@ object ClosedGroupsProtocol { private fun insertIncomingInfoMessage(context: Context, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long) { - val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)))) + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID))) .setType(type0) .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)), GroupType.SIGNAL, name, members.toList(), null, admins.toList()) + val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(groupID), GroupType.SIGNAL, name, members.toList(), null, admins.toList()) val m = IncomingTextMessage(Address.fromSerialized(groupID), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true) val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") val smsDB = DatabaseFactory.getSmsDatabase(context) @@ -473,7 +472,7 @@ object ClosedGroupsProtocol { members: Collection, admins: Collection, threadID: Long) { val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID)))) + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID))) .setType(type) .setName(name) .addAllMembers(members) From cb1aad425a7c54bc14658a1d83b400c0999fb7ac Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 13 Aug 2020 09:31:04 +1000 Subject: [PATCH 36/38] Fix group leaving --- .../loki/protocol/ClosedGroupsProtocol.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 0cb029a5d4..21ebe1a761 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -160,6 +160,7 @@ object ClosedGroupsProtocol { // send it out to all members (minus the removed ones) using established channels. if (isUserLeaving) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) } else { // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) @@ -170,16 +171,15 @@ object ClosedGroupsProtocol { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) } } // Update the group groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - val type = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) } @JvmStatic @@ -288,18 +288,20 @@ object ClosedGroupsProtocol { // 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 wasUserRemoved = !members.contains(userPublicKey) - if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) { + val wasCurrentUserRemoved = !members.contains(userPublicKey) + val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() + if (wasAnyUserRemoved) { sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) - if (wasUserRemoved) { + if (wasCurrentUserRemoved) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) } else { establishSessionsWithMembersIfNeeded(context, members) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) for (member in members) { val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) } } @@ -308,8 +310,8 @@ object ClosedGroupsProtocol { groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user - val type0 = if (wasUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE - val type1 = if (wasUserRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE + val type0 = if (wasAnyUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val type1 = if (wasAnyUserRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertIncomingInfoMessage(context, groupID, type0, type1, name, members, admins, threadID) } From 865f4b90ad22c96bff6832c952562b83954f45b9 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 13 Aug 2020 10:45:24 +1000 Subject: [PATCH 37/38] Disable SSKs for now --- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 21ebe1a761..d43492b48b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -30,7 +30,7 @@ import org.whispersystems.signalservice.loki.utilities.toHexString import java.util.* object ClosedGroupsProtocol { - val isSharedSenderKeysEnabled = true + val isSharedSenderKeysEnabled = false public fun createClosedGroup(context: Context, name: String, members: Collection): String { // Prepare From f8759246822150a14cc72ec749bb623f62c244a6 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 14 Aug 2020 08:51:46 +1000 Subject: [PATCH 38/38] Improve Chinese (simplified) translation --- res/values-zh-rCN/strings.xml | 112 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 9ae1019ebd..6cb9408701 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -1274,50 +1274,50 @@ 继续 复制 - 无效的网址 + 无效的链接 复制到剪贴板 无法链接设备。 下一步 - 分享 + 共享 无效的Session ID 取消 您的Session ID 您的Session从这里开始... - 注册Session ID + 创建Session ID 继续使用您的Session ID - 链接到现有帐号 - 您的设备已成功断开链接 + 关联现有帐号 + 您的设备已成功取消关联 什么是Session? Session是一个去中心化的加密消息应用。 - 所以Session不会收集我的个人信息或对话原始数据?怎么做到的?。 + 所以Session不会收集我的个人信息或者对话元数据?怎么做到的? 通过结合高效的匿名路由和端到端的加密技术。 - 好朋友就要与朋友使用能够保证信息安全的聊天工具,不用谢啦 + 好朋友之间就要使用能够保证信息安全的聊天工具,不用谢啦! - 向您的新Session ID打个招呼吧 - Session ID是其他用户需要与您聊天时使用的独一无二的地址。与您的真实身份无关,Session ID的设计是完全是匿名和私有的。 + 向您的Session ID打个招呼吧 + 您的Session ID是其他用户在与您聊天时使用的独一无二的地址。Session ID与您的真实身份无关,它在设计上完全是匿名且私密的。 复制到剪贴板 恢复您的帐号 在您重新登陆并需要恢复账户时,请输入您注册帐号时的恢复口令。 输入您的恢复口令 - 链接设备 + 关联设备 输入Session ID 扫描二维码 - 在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始链接过程。 + 在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始关联。 - 链接您的设备 - 在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始链接过程。 + 关联您的设备 + 在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始关联。 输入Session ID - 选择您的显示名称 - 使用Session时,这就是您的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。 - 输入显示名称 - 请选择一个显示名称 - 请选择一个仅包含 az,AZ,0-9 和_字符的显示名称 - 请选择一个较短的显示名称 + 选择您想显示的名称 + 这就是您在使用Session时的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。 + 输入您想显示的名称 + 请设定一个名称 + 请设定一个仅包含 a-z,A-Z,0-9 和 _ 的名称 + 请设定一个较短的名称 推荐的选项 请选择一个选项 @@ -1331,15 +1331,15 @@ 您的恢复口令 这里是您的恢复口令 - 您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复Session ID。将您的恢复口令存储在安全的地方,不要将其提供给任何人。 + 您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复您的Session ID。请将您的恢复口令存储在安全的地方,不要将其提供给任何人。 长按显示内容 - 保存恢复短语以保护您的帐号安全 - 点击并按住遮盖住的单词以显示您的恢复短语,然后安全地存储它以保护Session ID。 - 确保将恢复短语存储在安全的地方 + 保存恢复口令以保护您的帐号安全 + 点击并按住遮盖住的单词以显示您的恢复口令,请将它安全地存储以保护您的Session ID。 + 请确保将恢复口令存储在安全的地方 路径 - Session会通过Session的分散网络中的多个服务节点跳转消息以隐藏IP。以下是国家您目前的消息连接跳转服务节点所在地: + Session会通过其去中心化网络中的多个服务节点跳转消息以隐藏IP。以下国家是您目前的消息连接跳转服务节点所在地: 入口节点 服务节点 @@ -1349,38 +1349,38 @@ 新建私人聊天 输入Session ID 扫描二维码 - 扫描另一用户的二维码以开始使用Session。您可以在帐号设置中点击二维码图标找到二维码。 + 扫描其他用户的二维码来发起对话。您可以在帐号设置中点击二维码图标找到二维码。 输入对方的Session ID - 用户可以通过进入帐号设置并点击共享Session ID来分享自己的Session ID,或通过共享其二维码来分享其Session ID。 + 用户可以通过进入帐号设置并点击“共享Session ID”来分享自己的Session ID,或通过共享其二维码来分享其Session ID。 Session需要摄像头访问权限才能扫描二维码 授予摄像头访问权限 创建私密群组 输入群组名称 - 私密群组最多支持 10 位成员,并提供与一对一对话相同的隐私保护。 + 私密群组最多支持10位成员,并提供与一对一对话相同的隐私保护。 您还没有任何联系人 开始对话 请输入群组名称 请输入较短的群组名称 - 请选择至少 2 位小组成员 - 私密群组成员不得超过 10 个 + 请选择至少2位群组成员 + 私密群组成员不得超过10个 您群组中的一位成员的Session ID无效 加入公开群组 无法加入群组 - 公开群组网址 + 公开群组链接 扫描二维码 扫描您想加入的公开群组的二维码 - 输入一个公开群组网址 + 输入公开群组链接 设置 - 输入显示的名称 - 请选择一个显示名称 - 请选择一个仅包含 az,AZ,0-9 和 _ 字符的显示名称 - 请选择一个较短的显示名称 + 输入您想显示的名称 + 请设定一个名称 + 请设定一个仅包含 a-z,A-Z,0-9 和 _ 的名称 + 请设定一个较短的名称 隐私 通知 聊天 @@ -1389,44 +1389,44 @@ 清除数据 通知 - 通知风格类型 + 通知风格 通知内容 隐私 - 聊天 + 会话 设备 达到设备限制 - 当前不允许链接多个设备。 - 无法断开链接设备。 - 您的设备已成功断开链接 - 无法链接设备。 - 您尚未链接任何设备 - 链接设备(测试版) + 当前不允许关联多个设备。 + 无法断开关联设备。 + 您的设备已成功取消关联 + 无法关联设备。 + 您尚未关联任何设备 + 关联设备(测试版) 通知选项 等待授权 - 设备链接授权 + 设备关联已授权 请检查以下单词是否与您其他设备上显示的单词匹配。 - 您的设备已成功链接 + 您的设备已成功关联 等待设备 - 收到链接请求 - 授权设备链接 - 在其他设备上下载Session,然后点击登陆页面屏幕底部的“链接到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。 + 收到关联请求 + 授权设备关联 + 在其他设备上下载Session,然后点击登陆页面屏幕底部的“关联到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。 请检查以下单词是否与您其他设备上显示的单词匹配。 - 创建设备关联时,请耐心等待。这可能需要一分钟的时间。 + 创建设备关联时,请耐心等待。这可能需要一分钟左右的时间。 授权 - 更换名字 - 断开设备链接 + 更换名称 + 断开设备关联 - 输入名字 + 输入名称 您的恢复口令 - 这是您的恢复口令。有了它,您可以将Session ID还原或迁移到新设备上。 + 这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。 清除所有数据 这将永久删除您的消息、对话和联系人。 @@ -1434,13 +1434,13 @@ 二维码 查看我的二维码 扫描二维码 - 扫描对方的二维码,与他们开始对话 + 扫描对方的二维码以发起对话 - 这是您的二维码。其他用户可以对其进行扫描以开始对话。 + 这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。 分享二维码 您要恢复与%s的对话吗? - 解散 + 取消 恢复 联系人