feat: add finishing of group push for initial group creation and saving group configs to DB. re-implement key signing messages properly for the signing config which doesn't override the config base

This commit is contained in:
0x330a 2023-09-11 18:07:23 +10:00
parent 2527ac2e21
commit 6067916da9
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
12 changed files with 174 additions and 68 deletions

View File

@ -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)

View File

@ -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<SessionId>): 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<SessionId>): Optional<Boolean> {
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<RawResponse>)
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<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) {

View File

@ -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)
}
}

View File

@ -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 = {},

View File

@ -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,

View File

@ -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);
}

View File

@ -288,8 +288,10 @@ class GroupKeysConfig(pointer: Long): ConfigBase(pointer), Closeable {
info: GroupInfoConfig,
members: GroupMembersConfig
): GroupKeysConfig
external fun storageNamespace(): Int
}
external fun groupKeys(): Stack<ByteArray>
external fun keyDump(): ByteArray
external fun loadKey(hash: String,
data: ByteArray,
msgId: ByteArray,

View File

@ -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<SessionId>): Long?
fun createNewGroup(groupName: String, groupDescription: String, members: Set<SessionId>): Optional<Boolean>
fun getMembers(groupPublicKey: String): List<network.loki.messenger.libsession_util.util.GroupMember>
// Groups

View File

@ -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

View File

@ -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<String> {
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 {

View File

@ -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<Snode, Exception> {
fun getSingleTargetSnode(publicKey: String): Promise<Snode, Exception> {
// 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<String, Any>()
// 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)

View File

@ -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 {