feat: adding basic invite for new groups handling and immediate response, need to add handling for response and actually sending the invites via UI possibly

This commit is contained in:
0x330a 2023-10-23 17:42:20 +11:00
parent 352246842f
commit 2448af0b73
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
13 changed files with 362 additions and 1378 deletions

View File

@ -35,6 +35,7 @@ import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
@ -50,6 +51,7 @@ 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
@ -76,6 +78,8 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
@ -1199,6 +1203,28 @@ open class Storage(
override fun getMembers(groupPublicKey: String): List<LibSessionGroupMember> =
configFactory.getGroupMemberConfig(SessionId.from(groupPublicKey))?.use { it.all() }?.toList() ?: emptyList()
override fun acceptClosedGroupInvite(groupId: SessionId, name: String, authData: ByteArray, invitingAdmin: SessionId) {
val recipient = Recipient.from(context, Address.fromSerialized(groupId.hexString()), false)
val profileManager = SSKEnvironment.shared.profileManager
val groups = configFactory.userGroups ?: return
val closedGroupInfo = GroupInfo.ClosedGroupInfo(
groupId, byteArrayOf(), authData, PRIORITY_VISIBLE
)
groups.set(closedGroupInfo)
profileManager.setName(context, recipient, name)
setRecipientApprovedMe(recipient, true)
setRecipientApproved(recipient, true)
getOrCreateThreadIdFor(recipient.address)
pollerFactory.pollerFor(groupId)?.start()
val invitingAdminAddress = Address.fromSerialized(invitingAdmin.hexString())
val inviteResponse = GroupUpdateInviteResponseMessage.newBuilder()
.setIsApproved(true)
val responseData = DataMessage.GroupUpdateMessage.newBuilder()
.setInviteResponse(inviteResponse)
val responseMessage = GroupUpdated(responseData.build())
MessageSender.send(responseMessage, invitingAdminAddress)
}
override fun setServerCapabilities(server: String, capabilities: List<String>) {
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
}

View File

@ -13,8 +13,9 @@ class PollerFactory(private val scope: CoroutineScope,
private val pollers = ConcurrentHashMap<SessionId, ClosedGroupPoller>()
fun pollerFor(sessionId: SessionId): ClosedGroupPoller? {
val activeGroup = configFactory.userGroups?.getClosedGroup(sessionId.hexString()) ?: return null
// TODO: add check for active group being invited / approved etc
// Check if the group is currently in our config, don't start if it isn't
configFactory.userGroups?.getClosedGroup(sessionId.hexString()) ?: return null
return pollers.getOrPut(sessionId) {
ClosedGroupPoller(scope + SupervisorJob(), sessionId, configFactory)
}

@ -1 +1 @@
Subproject commit cce0db485f5749a2891ce56170342c5b0e82272c
Subproject commit e4b0358a50a65796ac5e2da4938505bc8ebf0313

View File

@ -229,4 +229,18 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_currentHashes(JNIEn
env->CallObjectMethod(our_list, push, hash_bytes);
}
return our_list;
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_makeSubAccount(JNIEnv *env,
jobject thiz,
jobject session_id,
jboolean can_write,
jboolean can_delete) {
std::lock_guard lock{util::util_mutex_};
auto ptr = ptrToKeys(env, thiz);
auto deserialized_id = util::deserialize_session_id(env, session_id);
auto new_subaccount_key = ptr->swarm_make_subaccount(deserialized_id.data(), can_write, can_delete);
auto jbytes = util::bytes_from_ustring(env, new_subaccount_key);
return jbytes;
}

View File

@ -332,4 +332,6 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
external fun keys(): Stack<ByteArray>
external fun makeSubAccount(sessionId: SessionId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
}

View File

@ -35,6 +35,7 @@ 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
import network.loki.messenger.libsession_util.util.GroupMember as LibSessionGroupMember
interface StorageProtocol {
@ -157,7 +158,8 @@ interface StorageProtocol {
// Closed Groups
fun createNewGroup(groupName: String, groupDescription: String, members: Set<SessionId>): Optional<Recipient>
fun getMembers(groupPublicKey: String): List<network.loki.messenger.libsession_util.util.GroupMember>
fun getMembers(groupPublicKey: String): List<LibSessionGroupMember>
fun acceptClosedGroupInvite(groupId: SessionId, name: String, authData: ByteArray, invitingAdmin: SessionId)
// Groups
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>

View File

@ -0,0 +1,21 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.protos.SignalServiceProtos.Content
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
class GroupUpdated(val inner: GroupUpdateMessage): ControlMessage() {
companion object {
fun fromProto(message: GroupUpdateMessage): GroupUpdated = GroupUpdated(message)
}
override fun toProto(): Content {
val dataMessage = DataMessage.newBuilder()
.setGroupUpdateMessage(inner)
.build()
return Content.newBuilder()
.setDataMessage(dataMessage)
.build()
}
}

View File

@ -153,7 +153,8 @@ object MessageReceiver {
MessageRequestResponse.fromProto(proto) ?:
CallMessage.fromProto(proto) ?:
SharedConfigurationMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: run {
VisibleMessage.fromProto(proto) ?:
ClosedGroupMessage.fromProto(proto) ?: run {
throw Error.UnknownMessage
}
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString() }

View File

@ -12,6 +12,7 @@ import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.messages.control.UnsendRequest
@ -122,9 +123,15 @@ object MessageSender {
message.profile = storage.getUserProfile()
}
// Convert it to protobuf
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
val proto = message.toProto()?.toBuilder() ?: throw Error.ProtoConversionFailed
if (message is GroupUpdated) {
// Add all cases where we have to attach profile
if (message.inner.hasInviteResponse()) {
proto.mergeDataMessage(storage.getUserProfile().toProto())
}
}
// Serialize the protobuf
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.build().toByteArray())
// Envelope information
val kind: SignalServiceProtos.Envelope.Type
@ -157,7 +164,7 @@ object MessageSender {
}
is Destination.ClosedGroup -> {
val groupKeys = configFactory.getGroupKeysConfig(SessionId.from(destination.publicKey)) ?: throw Error.NoKeyPair
val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.toByteArray())
val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.build().toByteArray())
groupKeys.use { keys ->
keys.encrypt(envelope.toByteArray())
}
@ -189,6 +196,7 @@ object MessageSender {
val storage = MessagingModuleConfiguration.shared.storage
val configFactory = MessagingModuleConfiguration.shared.configFactory
val userPublicKey = storage.getUserPublicKey()
val ourProfile = storage.getUserProfile()
// recipient will be set later, so initialize it as a function here
val isSelfSend = { message.recipient == userPublicKey }

View File

@ -13,6 +13,7 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.TypingIndicator
@ -51,6 +52,7 @@ import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import java.security.MessageDigest
import java.security.SignatureException
import java.util.LinkedList
import kotlin.math.min
@ -68,6 +70,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message)
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
is GroupUpdated -> handleNewLibSessionClosedGroupMessage(message)
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message)
is DataExtractionNotification -> handleDataExtractionNotification(message)
is ConfigurationMessage -> handleConfigurationMessage(message)
@ -516,6 +519,48 @@ private fun ClosedGroupControlMessage.getPublicKey(): String = kind!!.let { when
is ClosedGroupControlMessage.Kind.NameChange -> groupPublicKey!!
}}
private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated) {
when {
message.inner.hasInviteMessage() -> handleNewLibSessionClosedGroupMessage(message)
message.inner.hasInviteResponse() -> handleInviteResponse(message)
}
}
private fun MessageReceiver.handleInviteResponse(message: GroupUpdated) {
val sender = message.sender!!
// val profile = message // maybe we do need data to be the inner so we can access profile
val storage = MessagingModuleConfiguration.shared.storage
val approved = message.inner.inviteResponse.isApproved
}
private fun MessageReceiver.handleNewLibSessionClosedGroupMessage(message: GroupUpdated) {
val storage = MessagingModuleConfiguration.shared.storage
val ourUserId = storage.getUserPublicKey()!!
val invite = message.inner.inviteMessage
val groupId = SessionId.from(invite.groupSessionId)
verifyAdminSignature(groupId, invite.adminSignature.toByteArray(), "INVITE"+ourUserId+message.sentTimestamp!!)
val adminId = SessionId.from(message.sender!!)
// TODO: add the pending invite logic after testing initial group signing / message adding / encryption works for members as well
// add the group
storage.acceptClosedGroupInvite(groupId, invite.name, invite.memberAuthData.toByteArray(), adminId)
}
/**
* Does nothing on successful signature verification, throws otherwise.
* Assumes the signer is using the ed25519 group key signing key
* @param groupSessionId the SessionId of the group to check the signature against
* @param signatureData the byte array supplied to us through a protobuf message from the admin
* @param messageToValidate the expected values used for this signature generation, often something like `INVITE||{inviteeSessionId}||{timestamp}`
* @throws SignatureException if signature cannot be verified with given parameters
*/
private fun verifyAdminSignature(groupSessionId: SessionId, signatureData: ByteArray, messageToValidate: String) {
val groupPubKey = groupSessionId.pubKeyBytes
if (!SodiumUtilities.verifySignature(signatureData, groupPubKey, messageToValidate.encodeToByteArray())) {
throw SignatureException("Verification failed for signature data")
}
}
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)

View File

@ -230,4 +230,11 @@ object SodiumUtilities {
} else null
}
/**
* Returns true only if the signature verified successfully
*/
fun verifySignature(signature: ByteArray, publicKey: ByteArray, messageToVerify: ByteArray): Boolean {
return sodium.cryptoSignVerifyDetached(signature, messageToVerify, messageToVerify.size, publicKey)
}
}

View File

@ -120,13 +120,6 @@ message DataMessage {
required string name = 3;
}
message GroupMessage {
optional GroupDeleteMessage deleteMessage = 31;
optional GroupMemberLeftMessage memberLeftMessage = 32;
optional GroupInviteMessage inviteMessage = 33;
optional GroupPromoteMessage promoteMessage = 34;
}
// New closed group update messages
message GroupUpdateMessage {
optional GroupUpdateInviteMessage inviteMessage = 1;
@ -274,7 +267,7 @@ message DataMessage {
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
optional GroupMessage groupMessage = 120;
optional GroupUpdateMessage groupUpdateMessage = 120;
}
message GroupDeleteMessage {