Various issues

This commit is contained in:
SessionHero01 2024-10-02 15:17:13 +10:00
parent 3faae5ddbe
commit 1f5fde0d9a
No known key found for this signature in database
13 changed files with 133 additions and 104 deletions

View File

@ -41,6 +41,7 @@ import org.session.libsession.utilities.MutableUserConfigs
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.UserConfigType
import org.session.libsession.utilities.UserConfigs
import org.session.libsession.utilities.getClosedGroup
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Hex
@ -299,11 +300,7 @@ class ConfigFactory @Inject constructor(
override fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T {
val configs = groupConfigs.getOrPut(groupId) {
val groupAdminKey = requireNotNull(withUserConfigs {
it.userGroups.getClosedGroup(groupId.hexString)
}) {
"Group not found"
}.adminKey
val groupAdminKey = getClosedGroup(groupId)?.adminKey
GroupConfigsImpl(
requiresCurrentUserED25519SecKey(),
@ -318,7 +315,14 @@ class ConfigFactory @Inject constructor(
}
}
private fun <T> doWithMutableGroupConfigs(groupId: AccountId, cb: (GroupConfigsImpl) -> Pair<T, Boolean>): T {
private fun <T> doWithMutableGroupConfigs(
groupId: AccountId,
recreateConfigInstances: Boolean,
cb: (GroupConfigsImpl) -> Pair<T, Boolean>): T {
if (recreateConfigInstances) {
groupConfigs.remove(groupId)
}
val (result, changed) = withGroupConfigs(groupId) { configs ->
cb(configs as GroupConfigsImpl)
}
@ -336,9 +340,10 @@ class ConfigFactory @Inject constructor(
override fun <T> withMutableGroupConfigs(
groupId: AccountId,
recreateConfigInstances: Boolean,
cb: (MutableGroupConfigs) -> T
): T {
return doWithMutableGroupConfigs(groupId) {
return doWithMutableGroupConfigs(recreateConfigInstances = recreateConfigInstances, groupId = groupId) {
cb(it) to it.dumpIfNeeded()
}
}
@ -376,7 +381,7 @@ class ConfigFactory @Inject constructor(
info: List<ConfigMessage>,
members: List<ConfigMessage>
) {
doWithMutableGroupConfigs(groupId) { configs ->
doWithMutableGroupConfigs(groupId, false) { configs ->
// Keys must be loaded first as they are used to decrypt the other config messages
val keysLoaded = keys.fold(false) { acc, msg ->
configs.groupKeys.loadKey(msg.data, msg.hash, msg.timestamp, configs.groupInfo.pointer, configs.groupMembers.pointer) || acc
@ -424,7 +429,7 @@ class ConfigFactory @Inject constructor(
return
}
doWithMutableGroupConfigs(groupId) { configs ->
doWithMutableGroupConfigs(groupId, false) { configs ->
members?.let { (push, result) -> configs.groupMembers.confirmPushed(push.seqNo, result.hash) }
info?.let { (push, result) -> configs.groupInfo.confirmPushed(push.seqNo, result.hash) }
keysPush?.let { (hash, timestamp) ->

View File

@ -634,7 +634,7 @@ class GroupManagerV2Impl @Inject constructor(
pollerFactory.pollerFor(group.groupAccountId)?.start()
}
override suspend fun onReceiveInvitation(
override suspend fun handleInvitation(
groupId: AccountId,
groupName: String,
authData: ByteArray,
@ -663,7 +663,7 @@ class GroupManagerV2Impl @Inject constructor(
}
}
override suspend fun onReceivePromotion(
override suspend fun handlePromotion(
groupId: AccountId,
groupName: String,
adminKey: ByteArray,
@ -692,7 +692,7 @@ class GroupManagerV2Impl @Inject constructor(
}
// Update our promote state
configFactory.withMutableGroupConfigs(groupId) { configs ->
configFactory.withMutableGroupConfigs(recreateConfigInstances = true, groupId = groupId) { configs ->
configs.groupMembers.get(userAuth.accountId.hexString)?.let { member ->
configs.groupMembers.set(member.setPromoteSuccess())
}

View File

@ -60,7 +60,7 @@ internal class LoadingViewModel @Inject constructor(
val events = _events.asSharedFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
state.flatMapLatest {
when (it) {
State.LOADING -> progress(0f, 1f, TIMEOUT_TIME)

View File

@ -33,6 +33,7 @@ import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.retryWithUniformInterval
import java.util.concurrent.Executors
import javax.inject.Inject
@ -59,7 +60,6 @@ class ConfigSyncHandler @Inject constructor(
configFactory.configUpdateNotifications
.collect { changes ->
try {
when (changes) {
is ConfigUpdateNotification.GroupConfigsDeleted -> {
groupMutex.remove(changes.groupId)
@ -68,24 +68,32 @@ class ConfigSyncHandler @Inject constructor(
is ConfigUpdateNotification.GroupConfigsUpdated -> {
// Group config pushing is limited to its own dispatcher
launch {
groupMutex.getOrPut(changes.groupId) { Mutex() }.withLock {
pushGroupConfigsChangesIfNeeded(changes.groupId)
try {
retryWithUniformInterval {
groupMutex.getOrPut(changes.groupId) { Mutex() }.withLock {
pushGroupConfigsChangesIfNeeded(changes.groupId)
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to push group configs", e)
}
}
}
ConfigUpdateNotification.UserConfigs -> launch {
userMutex.withLock {
pushUserConfigChangesIfNeeded()
try {
retryWithUniformInterval {
userMutex.withLock {
pushUserConfigChangesIfNeeded()
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to push user configs", e)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error handling config update", e)
}
}
}
}
private suspend fun pushGroupConfigsChangesIfNeeded(groupId: AccountId) = coroutineScope {
@ -241,11 +249,17 @@ class ConfigSyncHandler @Inject constructor(
val pushTasks = pushes.map { (configType, configPush) ->
async {
(configType to configPush) to pushConfig(userAuth, snode, configPush, configType.namespace)
(configType to configPush) to pushConfig(
userAuth,
snode,
configPush,
configType.namespace
)
}
}
val pushResults = pushTasks.awaitAll().associate { it.first.first to (it.first.second to it.second) }
val pushResults =
pushTasks.awaitAll().associate { it.first.first to (it.first.second to it.second) }
Log.d(TAG, "Pushed ${pushResults.size} user configs")

View File

@ -3,7 +3,6 @@ package org.session.libsession.messaging.groups
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
import org.session.libsignal.utilities.AccountId
@ -36,7 +35,7 @@ interface GroupManagerV2 {
suspend fun promoteMember(group: AccountId, members: List<AccountId>)
suspend fun onReceiveInvitation(
suspend fun handleInvitation(
groupId: AccountId,
groupName: String,
authData: ByteArray,
@ -44,7 +43,7 @@ interface GroupManagerV2 {
inviteMessageHash: String?
)
suspend fun onReceivePromotion(
suspend fun handlePromotion(
groupId: AccountId,
groupName: String,
adminKey: ByteArray,

View File

@ -94,7 +94,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
}
private fun handleFailure(dispatcherName: String, error: Exception) {
Log.w(TAG, "Failed to send $message::class.simpleName.")
Log.w(TAG, "Failed to send $message::class.simpleName.", error)
val message = message as? VisibleMessage
if (message != null) {
if (!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) {

View File

@ -4,7 +4,9 @@ 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() {
class GroupUpdated @JvmOverloads constructor(
val inner: GroupUpdateMessage = GroupUpdateMessage.getDefaultInstance()
): ControlMessage() {
override fun isValid(): Boolean {
return true // TODO: add the validation here

View File

@ -6,7 +6,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.Sodium
import network.loki.messenger.libsession_util.util.afterSend
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.userAuth
import org.session.libsession.messaging.MessagingModuleConfiguration
@ -660,7 +659,7 @@ private fun handlePromotionMessage(message: GroupUpdated) {
GlobalScope.launch {
try {
MessagingModuleConfiguration.shared.groupManagerV2
.onReceivePromotion(
.handlePromotion(
groupId = AccountId(IdPrefix.GROUP, keyPair.pubKey),
groupName = promotion.name,
adminKey = keyPair.secretKey,
@ -703,7 +702,7 @@ private fun MessageReceiver.handleNewLibSessionClosedGroupMessage(message: Group
GlobalScope.launch {
try {
MessagingModuleConfiguration.shared.groupManagerV2
.onReceiveInvitation(
.handleInvitation(
groupId = groupId,
groupName = invite.name,
authData = invite.memberAuthData.toByteArray(),

View File

@ -42,6 +42,7 @@ class ClosedGroupPoller(
) {
companion object {
private const val POLL_INTERVAL = 3_000L
private const val POLL_ERROR_RETRY_DELAY = 10_000L
private const val TAG = "ClosedGroupPoller"
}
@ -54,34 +55,43 @@ class ClosedGroupPoller(
Log.d(TAG, "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}")
job?.cancel()
job = scope.launch(executor) {
var snode: Snode? = null
while (isActive) {
configFactoryProtocol.getClosedGroup(closedGroupSessionId) ?: break
try {
val swarmNodes = SnodeAPI.getSwarm(closedGroupSessionId.hexString).await().toMutableSet()
var currentSnode: Snode? = null
if (snode == null) {
Log.i(TAG, "No Snode, fetching one")
snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await()
}
while (isActive) {
if (currentSnode == null) {
check(swarmNodes.isNotEmpty()) { "No swarm nodes found" }
Log.d(TAG, "No current snode, getting a new one. Remaining in pool = ${swarmNodes.size - 1}")
currentSnode = swarmNodes.random()
swarmNodes.remove(currentSnode)
}
val nextPoll = runCatching { poll(snode!!) }
when {
nextPoll.isFailure -> {
Log.e(TAG, "Error polling closed group", nextPoll.exceptionOrNull())
// Clearing snode so we get a new one next time
snode = null
delay(POLL_INTERVAL)
}
nextPoll.getOrNull() == null -> {
// assume null poll time means don't continue polling, either the group has been deleted or something else
Log.d(TAG, "Stopping the closed group poller")
break
}
else -> {
delay(POLL_INTERVAL)
val result = runCatching { poll(currentSnode!!) }
when {
result.isSuccess -> {
delay(POLL_INTERVAL)
}
result.isFailure -> {
val error = result.exceptionOrNull()!!
if (error is CancellationException) {
throw error
}
Log.e(TAG, "Error polling closed group", error)
// Clearing snode so we get a new one next time
currentSnode = null
delay(POLL_INTERVAL)
}
}
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Error during group poller", e)
delay(POLL_ERROR_RETRY_DELAY)
}
}
}

View File

@ -296,22 +296,22 @@ object OnionRequestAPI {
is Destination.Snode -> destination.snode
is Destination.Server -> null
}
return getPath(snodeToExclude).bind { path ->
return getPath(snodeToExclude).map { path ->
guardSnode = path.first()
// Encrypt in reverse order, i.e. the destination first
OnionRequestEncryption.encryptPayloadForDestination(payload, destination, version).bind { r ->
OnionRequestEncryption.encryptPayloadForDestination(payload, destination, version).let { r ->
destinationSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
@Suppress("NAME_SHADOWING") var path = path
var rhs = destination
fun addLayer(): Promise<EncryptionResult, Exception> {
fun addLayer(): EncryptionResult {
return if (path.isEmpty()) {
Promise.of(encryptionResult)
encryptionResult
} else {
val lhs = Destination.Snode(path.last())
path = path.dropLast(1)
OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).let { r ->
encryptionResult = r
rhs = lhs
addLayer()

View File

@ -38,57 +38,53 @@ object OnionRequestEncryption {
payload: ByteArray,
destination: Destination,
version: Version
): Promise<EncryptionResult, Exception> {
return GlobalScope.asyncPromise {
val plaintext = if (version == Version.V4) {
payload
} else {
// Wrapping isn't needed for file server or open group onion requests
when (destination) {
is Destination.Snode -> encode(payload, mapOf("headers" to ""))
is Destination.Server -> payload
}
): EncryptionResult {
val plaintext = if (version == Version.V4) {
payload
} else {
// Wrapping isn't needed for file server or open group onion requests
when (destination) {
is Destination.Snode -> encode(payload, mapOf("headers" to ""))
is Destination.Server -> payload
}
val x25519PublicKey = when (destination) {
is Destination.Snode -> destination.snode.publicKeySet!!.x25519Key
is Destination.Server -> destination.x25519PublicKey
}
AESGCM.encrypt(plaintext, x25519PublicKey)
}
val x25519PublicKey = when (destination) {
is Destination.Snode -> destination.snode.publicKeySet!!.x25519Key
is Destination.Server -> destination.x25519PublicKey
}
return AESGCM.encrypt(plaintext, x25519PublicKey)
}
/**
* Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
*/
internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
return GlobalScope.asyncPromise {
val payload: MutableMap<String, Any> = when (rhs) {
is Destination.Snode -> {
mutableMapOf("destination" to rhs.snode.publicKeySet!!.ed25519Key)
}
is Destination.Server -> {
mutableMapOf(
"host" to rhs.host,
"target" to rhs.target,
"method" to "POST",
"protocol" to rhs.scheme,
"port" to rhs.port
)
}
internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): EncryptionResult {
val payload: MutableMap<String, Any> = when (rhs) {
is Destination.Snode -> {
mutableMapOf("destination" to rhs.snode.publicKeySet!!.ed25519Key)
}
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
val x25519PublicKey = when (lhs) {
is Destination.Snode -> {
lhs.snode.publicKeySet!!.x25519Key
}
is Destination.Server -> {
lhs.x25519PublicKey
}
is Destination.Server -> {
mutableMapOf(
"host" to rhs.host,
"target" to rhs.target,
"method" to "POST",
"protocol" to rhs.scheme,
"port" to rhs.port
)
}
val plaintext = encode(previousEncryptionResult.ciphertext, payload)
AESGCM.encrypt(plaintext, x25519PublicKey)
}
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
val x25519PublicKey = when (lhs) {
is Destination.Snode -> {
lhs.snode.publicKeySet!!.x25519Key
}
is Destination.Server -> {
lhs.x25519PublicKey
}
}
val plaintext = encode(previousEncryptionResult.ciphertext, payload)
return AESGCM.encrypt(plaintext, x25519PublicKey)
}
}

View File

@ -14,7 +14,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend fun <T, E: Throwable> Promise<T, E>.await(): T {
suspend inline fun <T, E: Throwable> Promise<T, E>.await(): T {
return suspendCoroutine { cont ->
success(cont::resume)
fail(cont::resumeWithException)

View File

@ -31,7 +31,11 @@ interface ConfigFactoryProtocol {
fun mergeUserConfigs(userConfigType: UserConfigType, messages: List<ConfigMessage>)
fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T
fun <T> withMutableGroupConfigs(groupId: AccountId, cb: (MutableGroupConfigs) -> T): T
/**
* @param recreateConfigInstances If true, the group configs will be recreated before calling the callback. This is useful when you have received an admin key or otherwise.
*/
fun <T> withMutableGroupConfigs(groupId: AccountId, recreateConfigInstances: Boolean = false, cb: (MutableGroupConfigs) -> T): T
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean