diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt index 19a511bfd..86413341f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.core.content.contentValuesOf import androidx.core.database.getBlobOrNull import androidx.core.database.getLongOrNull +import androidx.sqlite.db.transaction +import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { @@ -20,6 +22,10 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co "CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));" private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?" + + val KEYS_VARIANT = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name + val INFO_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name + val MEMBER_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.name } fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) { @@ -33,6 +39,39 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey)) } + fun storeGroupConfigs(publicKey: String, keysConfig: ByteArray, infoConfig: ByteArray, memberConfig: ByteArray, timestamp: Long) { + val db = writableDatabase + db.transaction { + val keyContent = contentValuesOf( + VARIANT to KEYS_VARIANT, + PUBKEY to publicKey, + DATA to keysConfig, + TIMESTAMP to timestamp + ) + db.insertOrUpdate(TABLE_NAME, keyContent, VARIANT_AND_PUBKEY_WHERE, + arrayOf(KEYS_VARIANT, publicKey) + ) + val infoContent = contentValuesOf( + VARIANT to INFO_VARIANT, + PUBKEY to publicKey, + DATA to infoConfig, + TIMESTAMP to timestamp + ) + db.insertOrUpdate(TABLE_NAME, infoContent, VARIANT_AND_PUBKEY_WHERE, + arrayOf(INFO_VARIANT, publicKey) + ) + val memberContent = contentValuesOf( + VARIANT to MEMBER_VARIANT, + PUBKEY to publicKey, + DATA to memberConfig, + TIMESTAMP to timestamp + ) + db.insertOrUpdate(TABLE_NAME, memberContent, VARIANT_AND_PUBKEY_WHERE, + arrayOf(MEMBER_VARIANT, publicKey) + ) + } + } + fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? { val db = readableDatabase val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 7d38f542e..f048d2a96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -51,7 +51,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -62,7 +61,9 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.GroupRecord @@ -882,16 +883,18 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp) } - override suspend fun createNewGroup(groupName: String, groupDescription: String, members: Set): Long? { - val userGroups = configFactory.userGroups ?: return null - val ourSessionId = getUserPublicKey() ?: return null - val userKp = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + override fun createNewGroup(groupName: String, groupDescription: String, members: Set): Optional { + val userGroups = configFactory.userGroups ?: return Optional.absent() + val ourSessionId = getUserPublicKey() ?: return Optional.absent() + val userKp = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Optional.absent() + + val groupCreationTimestamp = SnodeAPI.nowWithOffset val group = userGroups.createGroup() val adminKey = group.adminKey userGroups.set(group) - val groupInfo = configFactory.getOrConstructGroupInfoConfig(group.groupSessionId) ?: return null - val groupMembers = configFactory.getOrConstructGroupMemberConfig(group.groupSessionId) ?: return null + val groupInfo = configFactory.getOrConstructGroupInfoConfig(group.groupSessionId) ?: return Optional.absent() + val groupMembers = configFactory.getOrConstructGroupMemberConfig(group.groupSessionId) ?: return Optional.absent() with (groupInfo) { setName(groupName) @@ -910,9 +913,68 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co members = groupMembers ) + val newGroupRecipient = group.groupSessionId.hexString() + val configTtl = 1 * 24 * 60 * 60 * 1000L // TODO: just testing here, 1 day so we don't fill large space on network // Test the sending + val keyPush = groupKeys.pendingPush() ?: return Optional.absent() + val keysSnodeMessage = SnodeMessage( + newGroupRecipient, + Base64.encodeBytes(keyPush), + configTtl, + groupCreationTimestamp + ) + val keysBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( + GroupKeysConfig.storageNamespace(), + keysSnodeMessage, + adminKey + ) + + val (infoPush, infoSeqNo) = groupInfo.push() + val infoSnodeMessage = SnodeMessage( + newGroupRecipient, + Base64.encodeBytes(keyPush), + configTtl, + groupCreationTimestamp + ) + val infoBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( + groupInfo.configNamespace(), + infoSnodeMessage, + adminKey + ) + + val (memberPush, memberSeqNo) = groupMembers.push() + val memberSnodeMessage = SnodeMessage( + newGroupRecipient, + Base64.encodeBytes(memberPush), + configTtl, + groupCreationTimestamp + ) + val memberBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( + groupMembers.configNamespace(), + memberSnodeMessage, + adminKey + ) + try { - MessageSender.sendConfig(Destination.ClosedGroup(group.groupSessionId.hexString()), groupInfo, adminKey) + val snode = SnodeAPI.getSingleTargetSnode(newGroupRecipient).get() + val response = SnodeAPI.getRawBatchResponse( + snode, + newGroupRecipient, + listOf(keysBatchInfo, infoBatchInfo, memberBatchInfo), + true + ).get() + + @Suppress("UNCHECKED_CAST") + val responseList = (response["results"] as List) + + val keyResponse = responseList[0] + val infoResponse = responseList[1] + val memberResponse = responseList[2] + // TODO: check response success + configFactory.saveGroupConfigs(groupKeys, groupInfo, groupMembers) // now check poller to be all + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + Log.d("Group Config", "Saved group config for $newGroupRecipient") + return Optional.of(true) } catch (e: Exception) { Log.e("Group Config", e) Log.e("Group Config", "Deleting group from our group") @@ -920,7 +982,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co userGroups.erase(group) } - return 0 + return Optional.absent() } override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index 109727652..d5db05df6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -343,4 +343,13 @@ class ConfigFactory( return (changeTimestampMs >= (lastUpdateTimestampMs - ConfigFactory.configChangeBufferPeriod)) } + override fun saveGroupConfigs( + groupKeys: GroupKeysConfig, + groupInfo: GroupInfoConfig, + groupMembers: GroupMembersConfig + ) { + val pubKey = groupInfo.id().hexString() + val timestamp = SnodeAPI.nowWithOffset + configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index ff2791e35..1562054ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -44,6 +44,7 @@ import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.SessionId import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.EditableAvatar import org.thoughtcrime.securesms.ui.NavigationBar import org.thoughtcrime.securesms.ui.PreviewTheme @@ -87,20 +88,22 @@ class CreateGroupFragment : Fragment() { fun CreateGroupScreen(viewState: ViewState, createGroupState: CreateGroupState, modifier: Modifier = Modifier) { - CreateGroup( - viewState, - createGroupState, - onCreate = { newGroup -> - // launch something to create here - viewModel.tryCreateGroup(newGroup) - }, - onClose = { - delegate.onDialogClosePressed() - }, - onBack = { - delegate.onDialogBackPressed() - } - ) + AppTheme { + CreateGroup( + viewState, + createGroupState, + onCreate = { newGroup -> + // launch something to create here + viewModel.tryCreateGroup(newGroup) + }, + onClose = { + delegate.onDialogClosePressed() + }, + onBack = { + delegate.onDialogBackPressed() + } + ) + } } data class ViewState( @@ -221,7 +224,8 @@ fun ClosedGroupPreview( ) { PreviewTheme(themeResId) { CreateGroup( - CreateGroupState("Group Name", "Test Group Description", emptySet()), + viewState = CreateGroupFragment.ViewState(false, null, null), + createGroupState = CreateGroupState("Group Name", "Test Group Description", emptySet()), onCreate = {}, onClose = {}, onBack = {}, diff --git a/libsession-util/src/main/cpp/group_keys.cpp b/libsession-util/src/main/cpp/group_keys.cpp index eb5a8b867..43dc98e30 100644 --- a/libsession-util/src/main/cpp/group_keys.cpp +++ b/libsession-util/src/main/cpp/group_keys.cpp @@ -2,6 +2,13 @@ #include "group_info.h" #include "group_members.h" +extern "C" +JNIEXPORT jint JNICALL + Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_storageNamespace(JNIEnv* env, + jobject thiz) { + return (jint)session::config::Namespace::GroupKeys; +} + extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newInstance(JNIEnv *env, diff --git a/libsession-util/src/main/cpp/group_members.h b/libsession-util/src/main/cpp/group_members.h index e38a94007..addf0d57f 100644 --- a/libsession-util/src/main/cpp/group_members.h +++ b/libsession-util/src/main/cpp/group_members.h @@ -4,7 +4,7 @@ #include "util.h" inline session::config::groups::Members* ptrToMembers(JNIEnv* env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/GroupMemberConfig"); + jclass configClass = env->FindClass("network/loki/messenger/libsession_util/GroupMembersConfig"); jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); return (session::config::groups::Members*) env->GetLongField(obj, pointerField); } diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index b1aed05b8..a38dc9716 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -288,8 +288,10 @@ class GroupKeysConfig(pointer: Long): ConfigBase(pointer), Closeable { info: GroupInfoConfig, members: GroupMembersConfig ): GroupKeysConfig + external fun storageNamespace(): Int } external fun groupKeys(): Stack + external fun keyDump(): ByteArray external fun loadKey(hash: String, data: ByteArray, msgId: ByteArray, diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 5eabe48ef..6e56af1a1 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -33,6 +33,7 @@ import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.SessionId +import org.session.libsignal.utilities.guava.Optional import network.loki.messenger.libsession_util.util.Contact as LibSessionContact interface StorageProtocol { @@ -155,7 +156,7 @@ interface StorageProtocol { fun setExpirationTimer(address: String, duration: Int) // Closed Groups - suspend fun createNewGroup(groupName: String, groupDescription: String, members: Set): Long? + fun createNewGroup(groupName: String, groupDescription: String, members: Set): Optional fun getMembers(groupPublicKey: String): List // Groups diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index fe4df6b72..01d7e0a55 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -69,7 +69,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job { // return a list of batch request objects val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true) val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo( - destination.destinationPublicKey(), config.configNamespace(), snodeMessage ) ?: return@map null // this entry will be null otherwise diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 52787e7ab..839d62010 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,7 +1,5 @@ package org.session.libsession.messaging.sending_receiving -import androidx.annotation.WorkerThread -import network.loki.messenger.libsession_util.ConfigBase import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration @@ -73,25 +71,6 @@ object MessageSender { } } - // New closed groups and configs not requiring additional overhead (already handled by libsession) - @WorkerThread - fun sendConfig(destination: Destination, config: ConfigBase, signingKey: ByteArray): Result { - if (destination !is Destination.ClosedGroup) return Result.failure(Error.InvalidDestination(destination)) - - val (bytes, _) = config.push() - - val testTtl = 30 * 24 * 60 * 60 * 1000L // 30 days - - // handle this error thrown case - val response = SnodeAPI.sendMessage(destination, bytes, testTtl, signingKey, config.configNamespace()) - - Log.d("Send Config", "Response is good") - - val hash = response["hash"] as? String ?: return Result.failure(Error("No returned hash of string type")) - - return Result.success(hash) - } - // One-on-One Chats & Closed Groups @Throws(Exception::class) fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage { diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 87828e682..ad392d991 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -19,8 +19,6 @@ import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.task import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.messages.Destination -import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.database.LokiAPIDatabaseProtocol @@ -219,7 +217,7 @@ object SnodeAPI { } } - internal fun getSingleTargetSnode(publicKey: String): Promise { + fun getSingleTargetSnode(publicKey: String): Promise { // SecureRandom() should be cryptographically secure return getSwarm(publicKey).map { it.shuffled(SecureRandom()).random() } } @@ -374,7 +372,7 @@ object SnodeAPI { return invoke(Snode.Method.Retrieve, snode, parameters, publicKey) } - fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? { + fun buildAuthenticatedStoreBatchInfo(namespace: Int, message: SnodeMessage, signingKey: ByteArray, ed25519PubKey: String? = null): SnodeBatchRequestInfo { val params = mutableMapOf() // load the message data params into the sub request // currently loads: @@ -388,13 +386,6 @@ object SnodeAPI { // used for sig generation since it is also the value used in timestamp parameter val messageTimestamp = message.timestamp - val userEd25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null - } catch (e: Exception) { - return null - } - - val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString val signature = ByteArray(Sign.BYTES) val verificationData = "store$namespace$messageTimestamp".toByteArray() try { @@ -402,13 +393,15 @@ object SnodeAPI { signature, verificationData, verificationData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes + signingKey ) } catch (e: Exception) { Log.e("Loki", "Signing data failed with user secret key", e) } // timestamp already set - params["pubkey_ed25519"] = ed25519PublicKey + if (ed25519PubKey != null) { + params["pubkey_ed25519"] = ed25519PubKey + } params["signature"] = Base64.encodeBytes(signature) return SnodeBatchRequestInfo( Snode.Method.SendMessage.rawValue, @@ -417,6 +410,16 @@ object SnodeAPI { ) } + fun buildAuthenticatedStoreBatchInfo(namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? { + val userEd25519KeyPair = try { + MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + } catch (e: Exception) { + return null + } + val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString + return buildAuthenticatedStoreBatchInfo(namespace, message, userEd25519KeyPair.secretKey.asBytes, ed25519PublicKey) + } + /** * Message hashes can be shared across multiple namespaces (for a single public key destination) * @param publicKey the destination's identity public key to delete from (05...) @@ -634,11 +637,8 @@ object SnodeAPI { } @WorkerThread - fun sendMessage(destination: Destination, rawMessage: ByteArray, ttl: Long, signingKey: ByteArray, namespace: Int): RawResponse { - val pubKey = when (destination) { - is Destination.ClosedGroup -> destination.publicKey - else -> throw MessageSender.Error.InvalidDestination(destination) - } + fun sendAuthenticatedMessage(message: SnodeMessage, signingKey: ByteArray, namespace: Int): RawResponse { + val pubKey = message.recipient return retryIfNeeded(maxRetryCount) { val timestamp = nowWithOffset @@ -654,8 +654,7 @@ object SnodeAPI { val parameters = mapOf( "pubKey" to pubKey, - "data" to Base64.encodeBytes(rawMessage), - "ttl" to ttl.toString(), + "data" to message.data, "timestamp" to timestamp.toString(), "sig_timestamp" to timestamp.toString(), "signature" to Base64.encodeBytes(verificationData) diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 93346351a..0028aac69 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -25,6 +25,11 @@ interface ConfigFactoryProtocol { fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean + fun saveGroupConfigs( + groupKeys: GroupKeysConfig, + groupInfo: GroupInfoConfig, + groupMembers: GroupMembersConfig + ) } interface ConfigFactoryUpdateListener {