diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index b5a3919eb9..f7642c7e77 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers @@ -89,13 +88,7 @@ internal class DefaultVerificationService @Inject constructor( private val stateMachine: VerificationActor init { - val channel = Channel( - capacity = Channel.UNLIMITED, - ) - stateMachine = verificationActorFactory.create(channel) - executorScope.launch { - for (msg in channel) stateMachine.onReceive(msg) - } + stateMachine = verificationActorFactory.create(executorScope) } // It's obselete but not deprecated // It's ok as it will be replaced by rust implementation diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt index 35dab90524..2e7cd0b415 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt @@ -16,11 +16,15 @@ package org.matrix.android.sdk.internal.crypto.verification +import androidx.annotation.VisibleForTesting import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull @@ -31,8 +35,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest @@ -45,12 +47,9 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent @@ -69,14 +68,11 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_REC import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import org.matrix.android.sdk.internal.crypto.model.rest.toValue import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData import org.matrix.android.sdk.internal.crypto.verification.qrcode.generateSharedSecretV2 import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import java.util.Locale @@ -91,21 +87,34 @@ import java.util.Locale private val loggerTag = LoggerTag("Verification", LoggerTag.CRYPTO) internal class VerificationActor @AssistedInject constructor( - @Assisted private val channel: Channel, + @Assisted private val scope: CoroutineScope, private val clock: Clock, @UserId private val myUserId: String, private val cryptoStore: IMXCryptoStore, - private val sendVerificationMessageTask: SendVerificationMessageTask, - private val localEchoEventFactory: LocalEchoEventFactory, - private val sendToDeviceTask: SendToDeviceTask, private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: dagger.Lazy, private val secretShareManager: SecretShareManager, + private val transportLayer: VerificationTransportLayer, ) { @AssistedFactory interface Factory { - fun create(channel: Channel): VerificationActor + fun create(scope: CoroutineScope): VerificationActor + } + + @VisibleForTesting + val channel = Channel( + capacity = Channel.UNLIMITED, + ) + + init { + scope.launch { + Timber.e("VALR BEFORE") + for (msg in channel) { + onReceive(msg) + } + Timber.e("VALR NNNNNNNN") + } } // map [sender : [transaction]] @@ -121,7 +130,10 @@ internal class VerificationActor @AssistedInject constructor( */ private val pendingRequests = HashMap>() - val eventFlow = MutableSharedFlow(replay = 0) + // Replaces the typical list of listeners pattern. Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity + // We don't want to use emit as it would block if no listener is subscribed + // So we should use try emit using extraBufferCapacity, we use drop_oldest instead of suspend. + val eventFlow = MutableSharedFlow(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST) suspend fun send(intent: VerificationIntent) { channel.send(intent) @@ -230,7 +242,7 @@ internal class VerificationActor @AssistedInject constructor( handleIncomingRequest(msg) } is VerificationIntent.ActionReadyRequest -> { - handleReadyRequest(msg) + handleActionReadyRequest(msg) } is VerificationIntent.ActionStartSasVerification -> { handleSasStart(msg) @@ -323,9 +335,9 @@ internal class VerificationActor @AssistedInject constructor( if (existingTx != null) { existingTx.state = SasTransactionState.Cancelled(cancelCode, false) txMap[msg.fromUser]?.remove(msg.validCancel.transactionId) - eventFlow.emit(VerificationEvent.TransactionUpdated(existingTx)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existingTx)) } - eventFlow.emit(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) } } is VerificationIntent.OnReadyByAnotherOfMySessionReceived -> { @@ -334,6 +346,12 @@ internal class VerificationActor @AssistedInject constructor( } } + private fun dispatchUpdate(update: VerificationEvent) { + // We don't want to block on emit. + // If no subscriber there is a small buffer and too old would be dropped + eventFlow.tryEmit(update) + } + private suspend fun handleIncomingRequest(msg: VerificationIntent.OnVerificationRequestReceived) { val pendingVerificationRequest = KotlinVerificationRequest( requestId = msg.validRequestInfo.transactionId, @@ -395,7 +413,7 @@ internal class VerificationActor @AssistedInject constructor( } } matchingRequest.state = EVerificationState.Started - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) } private suspend fun handleReceiveStartForQR(request: KotlinVerificationRequest, reciprocate: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { @@ -518,7 +536,7 @@ internal class VerificationActor @AssistedInject constructor( // cancel if network error (would not send back a cancel but at least current user will see feedback?) try { - sendToOther(request, EventType.KEY_VERIFICATION_ACCEPT, accept) + transportLayer.sendToOther(request, EventType.KEY_VERIFICATION_ACCEPT, accept) } catch (failure: Throwable) { Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}] Failed to send accept for ${request.requestId}") @@ -569,7 +587,7 @@ internal class VerificationActor @AssistedInject constructor( Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}]: Sending my key $pubKey") } - sendToOther( + transportLayer.sendToOther( matchingRequest, EventType.KEY_VERIFICATION_KEY, keyMessage, @@ -578,13 +596,13 @@ internal class VerificationActor @AssistedInject constructor( existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) matchingRequest.cancelCode = CancelCode.UserError matchingRequest.state = EVerificationState.Cancelled - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) return } existing.accepted = accept existing.state = SasTransactionState.SasKeySent - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) } private suspend fun handleSasStart(msg: VerificationIntent.ActionStartSasVerification) { @@ -617,7 +635,7 @@ internal class VerificationActor @AssistedInject constructor( requestId = msg.requestId ) - sendToOther( + transportLayer.sendToOther( matchingRequest, EventType.KEY_VERIFICATION_START, startMessage, @@ -643,7 +661,7 @@ internal class VerificationActor @AssistedInject constructor( ) matchingRequest.state = EVerificationState.WeStarted - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) addTransaction(tx) msg.deferred.complete(tx) @@ -805,7 +823,7 @@ internal class VerificationActor @AssistedInject constructor( } try { - sendToOther(matchingRequest, EventType.KEY_VERIFICATION_START, message) + transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_START, message) } catch (failure: Throwable) { Timber.tag(loggerTag.value) .d("[${myUserId.take(8)}] Failed to send reciprocate message") @@ -814,7 +832,7 @@ internal class VerificationActor @AssistedInject constructor( } matchingRequest.state = EVerificationState.WeStarted - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) val tx = KotlinQRVerification( channel = this.channel, @@ -881,7 +899,7 @@ internal class VerificationActor @AssistedInject constructor( val pubKey = existing.getSAS().publicKey val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey) try { - sendToOther( + transportLayer.sendToOther( matchingRequest, EventType.KEY_VERIFICATION_KEY, keyMessage, @@ -898,13 +916,13 @@ internal class VerificationActor @AssistedInject constructor( Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}]:i EMOJI CODE ${existing.getEmojiCodeRepresentation().joinToString(" ") { it.emoji }}") } - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) } catch (failure: Throwable) { existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) matchingRequest.state = EVerificationState.Cancelled matchingRequest.cancelCode = CancelCode.UserError - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) return } } else { @@ -931,7 +949,7 @@ internal class VerificationActor @AssistedInject constructor( } existing.calculateSASBytes(otherKey) existing.state = SasTransactionState.SasShortCodeReady - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) if (BuildConfig.LOG_PRIVATE_DATA) { Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}]:o CODE ${existing.getDecimalCodeRepresentation()}") @@ -966,7 +984,7 @@ internal class VerificationActor @AssistedInject constructor( // I can start verify, store it existing.theirMac = msg.validMac existing.state = SasTransactionState.SasMacReceived(false) - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) } else -> { // it's a wrong state should cancel? @@ -1026,12 +1044,12 @@ internal class VerificationActor @AssistedInject constructor( if (isCorrectState) { existing.state = SasTransactionState.Done(true) - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) // we can forget about it txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) // XXX whatabout waiting for done? matchingRequest.state = EVerificationState.Done - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) } else { // TODO cancel? Timber.tag(loggerTag.value) @@ -1048,7 +1066,7 @@ internal class VerificationActor @AssistedInject constructor( } QRCodeVerificationState.WaitingForOtherDone -> { matchingRequest.state = EVerificationState.Done - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) } else -> { Timber.tag(loggerTag.value) @@ -1101,7 +1119,7 @@ internal class VerificationActor @AssistedInject constructor( } } - sendToOther( + transportLayer.sendToOther( matchingRequest, EventType.KEY_VERIFICATION_DONE, if (matchingRequest.roomId != null) { @@ -1117,11 +1135,11 @@ internal class VerificationActor @AssistedInject constructor( ) existing.state = QRCodeVerificationState.Done - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) // we can forget about it txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) matchingRequest.state = EVerificationState.WaitingForDone - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) if (shouldRequestSecret) { matchingRequest.otherDeviceId()?.let { otherDeviceId -> @@ -1167,7 +1185,7 @@ internal class VerificationActor @AssistedInject constructor( val macMsg = KotlinSasTransaction.sasMacMessage(matchingRequest.roomId != null, transactionId, macInfo) try { - sendToOther(matchingRequest, EventType.KEY_VERIFICATION_MAC, macMsg) + transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_MAC, macMsg) } catch (failure: Throwable) { // it's a network problem, we don't need to cancel, user can retry? msg.deferred.completeExceptionally(failure) @@ -1180,7 +1198,7 @@ internal class VerificationActor @AssistedInject constructor( finalizeSasTransaction(existing, theirMac, matchingRequest, transactionId) } else { existing.state = SasTransactionState.SasMacSent - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) } msg.deferred.complete(Unit) @@ -1237,7 +1255,7 @@ internal class VerificationActor @AssistedInject constructor( } // we should send done and wait for done - sendToOther( + transportLayer.sendToOther( matchingRequest, EventType.KEY_VERIFICATION_DONE, if (matchingRequest.roomId != null) { @@ -1253,11 +1271,11 @@ internal class VerificationActor @AssistedInject constructor( ) existing.state = SasTransactionState.Done(false) - eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) + dispatchUpdate(VerificationEvent.TransactionUpdated(existing)) pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = existing txMap[matchingRequest.otherUserId]?.remove(transactionId) matchingRequest.state = EVerificationState.WaitingForDone - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) } KotlinSasTransaction.MacVerificationResult.MismatchKeys, KotlinSasTransaction.MacVerificationResult.MismatchMacCrossSigning, @@ -1268,7 +1286,7 @@ internal class VerificationActor @AssistedInject constructor( } } - private suspend fun handleReadyRequest(msg: VerificationIntent.ActionReadyRequest) { + private suspend fun handleActionReadyRequest(msg: VerificationIntent.ActionReadyRequest) { val existing = pendingRequests .flatMap { it.value } .firstOrNull { it.requestId == msg.transactionId } @@ -1317,7 +1335,7 @@ internal class VerificationActor @AssistedInject constructor( fromDevice = cryptoStore.getDeviceId() ) try { - sendToOther(existing, EventType.KEY_VERIFICATION_READY, message) + transportLayer.sendToOther(existing, EventType.KEY_VERIFICATION_READY, message) } catch (failure: Throwable) { msg.deferred.completeExceptionally(failure) return @@ -1326,7 +1344,9 @@ internal class VerificationActor @AssistedInject constructor( existing.readyInfo = readyInfo existing.qrCodeData = qrCodeData existing.state = EVerificationState.Ready - eventFlow.emit(VerificationEvent.RequestUpdated(existing.toPendingVerificationRequest())) + + // We want to try emit, if not this will suspend until someone consume the flow + dispatchUpdate(VerificationEvent.RequestUpdated(existing.toPendingVerificationRequest())) Timber.tag(loggerTag.value).v("Request ${msg.transactionId} updated $existing") msg.deferred.complete(existing.toPendingVerificationRequest()) @@ -1424,13 +1444,11 @@ internal class VerificationActor @AssistedInject constructor( timestamp = validInfo.timestamp, methods = validInfo.methods ) - val event = createEventAndLocalEcho( - localId = validLocalId, + val eventId = transportLayer.sendInRoom( type = EventType.MESSAGE, roomId = msg.roomId, content = info.toContent() ) - val eventId = sendEventInRoom(event) val request = KotlinVerificationRequest( requestId = eventId, incoming = false, @@ -1443,10 +1461,10 @@ internal class VerificationActor @AssistedInject constructor( } requestsForUser.add(request) msg.deferred.complete(request.toPendingVerificationRequest()) - eventFlow.emit(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) } else { val requestId = LocalEcho.createLocalEchoId() - sendToDeviceEvent( + transportLayer.sendToDeviceEvent( messageType = EventType.KEY_VERIFICATION_REQUEST, toSendToDeviceObject = KeyVerificationRequest( transactionId = requestId, @@ -1470,7 +1488,7 @@ internal class VerificationActor @AssistedInject constructor( } requestsForUser.add(request) msg.deferred.complete(request.toPendingVerificationRequest()) - eventFlow.emit(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) } } catch (failure: Throwable) { // some network problem @@ -1499,13 +1517,13 @@ internal class VerificationActor @AssistedInject constructor( Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") matchingRequest.state = EVerificationState.HandledByOtherSession - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) return } matchingRequest.readyInfo = msg.readyInfo matchingRequest.state = EVerificationState.Ready - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) // if (matchingRequest.readyInfo != null) { // // TODO we already received a ready, cancel? or ignore @@ -1530,7 +1548,7 @@ internal class VerificationActor @AssistedInject constructor( .orEmpty() try { - sendToDeviceEvent( + transportLayer.sendToDeviceEvent( EventType.KEY_VERIFICATION_CANCEL, KeyVerificationCancel( msg.transactionId, @@ -1556,7 +1574,7 @@ internal class VerificationActor @AssistedInject constructor( Timber.tag(loggerTag.value) .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") matchingRequest.state = EVerificationState.HandledByOtherSession - eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) return } @@ -1570,12 +1588,12 @@ internal class VerificationActor @AssistedInject constructor( // requestsForUser.removeAt(index) // } // requestsForUser.add(updated) -// eventFlow.emit(VerificationEvent.RequestUpdated(updated)) +// dispatchUpdate(VerificationEvent.RequestUpdated(updated)) // } private suspend fun dispatchRequestAdded(tx: KotlinVerificationRequest) { Timber.v("## SAS dispatchRequestAdded txId:${tx.requestId}") - eventFlow.emit(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest())) } // Utilities @@ -1655,77 +1673,77 @@ internal class VerificationActor @AssistedInject constructor( ) } - private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { - return Event( - roomId = roomId, - originServerTs = clock.epochMillis(), - senderId = myUserId, - eventId = localId, - type = type, - content = content, - unsignedData = UnsignedData(age = null, transactionId = localId) - ).also { - localEchoEventFactory.createLocalEcho(it) - } - } - - private suspend fun sendEventInRoom(event: Event): String { - return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId - } - - private suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List) { - // TODO currently to device verification messages are sent unencrypted - // as per spec not recommended - // > verification messages may be sent unencrypted, though this is not encouraged. - - val contentMap = MXUsersDevicesMap() - - targetDevices.forEach { - contentMap.setObject(otherUserId, it, toSendToDeviceObject) - } - - sendToDeviceTask - .execute(SendToDeviceTask.Params(messageType, contentMap)) - } - - suspend fun sendToOther( - request: KotlinVerificationRequest, - type: String, - verificationInfo: VerificationInfo<*>, - ) { - val roomId = request.roomId - if (roomId != null) { - val event = createEventAndLocalEcho( - type = type, - roomId = roomId, - content = verificationInfo.toEventContent()!! - ) - sendEventInRoom(event) - } else { - sendToDeviceEvent( - type, - verificationInfo.toSendToDeviceObject()!!, - request.otherUserId, - request.otherDeviceId()?.let { listOf(it) }.orEmpty() - ) - } - } +// private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { +// return Event( +// roomId = roomId, +// originServerTs = clock.epochMillis(), +// senderId = myUserId, +// eventId = localId, +// type = type, +// content = content, +// unsignedData = UnsignedData(age = null, transactionId = localId) +// ).also { +// localEchoEventFactory.createLocalEcho(it) +// } +// } +// +// private suspend fun sendEventInRoom(event: Event): String { +// return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId +// } +// +// private suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List) { +// // TODO currently to device verification messages are sent unencrypted +// // as per spec not recommended +// // > verification messages may be sent unencrypted, though this is not encouraged. +// +// val contentMap = MXUsersDevicesMap() +// +// targetDevices.forEach { +// contentMap.setObject(otherUserId, it, toSendToDeviceObject) +// } +// +// sendToDeviceTask +// .execute(SendToDeviceTask.Params(messageType, contentMap)) +// } +// +// suspend fun sendToOther( +// request: KotlinVerificationRequest, +// type: String, +// verificationInfo: VerificationInfo<*>, +// ) { +// val roomId = request.roomId +// if (roomId != null) { +// val event = createEventAndLocalEcho( +// type = type, +// roomId = roomId, +// content = verificationInfo.toEventContent()!! +// ) +// sendEventInRoom(event) +// } else { +// sendToDeviceEvent( +// type, +// verificationInfo.toSendToDeviceObject()!!, +// request.otherUserId, +// request.otherDeviceId()?.let { listOf(it) }.orEmpty() +// ) +// } +// } private suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) { request.state = EVerificationState.Cancelled request.cancelCode = code - eventFlow.emit(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) + dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) // should also update SAS/QR transaction getExistingTransaction(request.otherUserId, request.requestId)?.let { it.state = SasTransactionState.Cancelled(code, true) txMap[request.otherUserId]?.remove(request.requestId) - eventFlow.emit(VerificationEvent.TransactionUpdated(it)) + dispatchUpdate(VerificationEvent.TransactionUpdated(it)) } getExistingTransaction(request.otherUserId, request.requestId)?.let { it.state = QRCodeVerificationState.Cancelled txMap[request.otherUserId]?.remove(request.requestId) - eventFlow.emit(VerificationEvent.TransactionUpdated(it)) + dispatchUpdate(VerificationEvent.TransactionUpdated(it)) } cancelRequest(request.requestId, request.roomId, request.otherUserId, request.otherDeviceId(), code) @@ -1756,21 +1774,26 @@ internal class VerificationActor @AssistedInject constructor( private suspend fun cancelTransactionToDevice(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { Timber.d("## SAS canceling transaction $transactionId for reason $code") val cancelMessage = KeyVerificationCancel.create(transactionId, code) - val contentMap = MXUsersDevicesMap() - contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) - sendToDeviceTask - .execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) +// val contentMap = MXUsersDevicesMap() +// contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + transportLayer.sendToDeviceEvent( + messageType = EventType.KEY_VERIFICATION_CANCEL, + toSendToDeviceObject = cancelMessage, + otherUserId = otherUserId, + targetDevices = otherUserDeviceId?.let { listOf(it) } ?: emptyList() + ) +// sendToDeviceTask +// .execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) } private suspend fun cancelTransactionInRoom(roomId: String, transactionId: String, code: CancelCode) { Timber.d("## SAS canceling transaction $transactionId for reason $code") val cancelMessage = MessageVerificationCancelContent.create(transactionId, code) - val event = createEventAndLocalEcho( + transportLayer.sendInRoom( type = EventType.KEY_VERIFICATION_CANCEL, roomId = roomId, content = cancelMessage.toEventContent() ) - sendEventInRoom(event) } private fun hashUsingAgreedHashMethod(hashMethod: String?, toHash: String): String { @@ -1785,7 +1808,7 @@ internal class VerificationActor @AssistedInject constructor( private suspend fun addTransaction(tx: VerificationTransaction) { val txInnerMap = txMap.getOrPut(tx.otherUserId) { mutableMapOf() } txInnerMap[tx.transactionId] = tx - eventFlow.emit(VerificationEvent.TransactionAdded(tx)) + dispatchUpdate(VerificationEvent.TransactionAdded(tx)) } private inline fun getExistingTransaction(otherUserId: String, transactionId: String): T? { diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt new file mode 100644 index 0000000000..465480b445 --- /dev/null +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportLayer.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.UnsignedData +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +internal class VerificationTransportLayer @Inject constructor( + @UserId private val myUserId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, + private val localEchoEventFactory: LocalEchoEventFactory, + private val sendToDeviceTask: SendToDeviceTask, + private val clock: Clock, +) { + + suspend fun sendToOther( + request: KotlinVerificationRequest, + type: String, + verificationInfo: VerificationInfo<*>, + ) { + val roomId = request.roomId + if (roomId != null) { + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = verificationInfo.toEventContent()!! + ) + sendEventInRoom(event) + } else { + sendToDeviceEvent( + type, + verificationInfo.toSendToDeviceObject()!!, + request.otherUserId, + request.otherDeviceId()?.let { listOf(it) }.orEmpty() + ) + } + } + + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), + type: String, + roomId: String, + content: Content): Event { + return Event( + roomId = roomId, + originServerTs = clock.epochMillis(), + senderId = myUserId, + eventId = localId, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localId) + ).also { + localEchoEventFactory.createLocalEcho(it) + } + } + + suspend fun sendInRoom(localId: String = LocalEcho.createLocalEchoId(), + type: String, + roomId: String, + content: Content): String { + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = content + ) + return sendEventInRoom(event) + } + + suspend fun sendEventInRoom(event: Event): String { + return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId + } + + suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List) { + // TODO currently to device verification messages are sent unencrypted + // as per spec not recommended + // > verification messages may be sent unencrypted, though this is not encouraged. + + val contentMap = MXUsersDevicesMap() + + targetDevices.forEach { + contentMap.setObject(otherUserId, it, toSendToDeviceObject) + } + + sendToDeviceTask + .execute(SendToDeviceTask.Params(messageType, contentMap)) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt index 2e7b36ff63..03b0bf0bc5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt @@ -32,7 +32,7 @@ object CredentialsFixture { accessToken, refreshToken, homeServer, - deviceId, + deviceId ?: "", discoveryInformation, ) } diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt new file mode 100644 index 0000000000..5f731d7b53 --- /dev/null +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.MXCryptoAlgorithms +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +enum class StoreMode { + Alice, + Bob +} + +internal class FakeCryptoStoreForVerification(private val mode: StoreMode) { + + val instance = mockk() + + init { + every { instance.getDeviceId() } answers { + when (mode) { + StoreMode.Alice -> aliceDevice1Id + StoreMode.Bob -> bobDeviceId + } + } + + // order matters here but can't find any info in doc about that + every { instance.getUserDevice(any(), any()) } returns null + every { instance.getUserDevice(aliceMxId, aliceDevice1Id) } returns aliceFirstDevice + every { instance.getUserDevice(bobDeviceId, bobDeviceId) } returns aBobDevice + + every { instance.getCrossSigningInfo(aliceMxId) } answers { + + when (mode) { + StoreMode.Alice -> { + MXCrossSigningInfo( + aliceMxId, + listOf( + aliceMSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + aliceUSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + aliceSSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + ), + wasTrustedOnce = true + ) + } + StoreMode.Bob -> { + MXCrossSigningInfo( + aliceMxId, + listOf( + aliceMSKBase.copy(trustLevel = DeviceTrustLevel(false, false)), + aliceUSKBase.copy(trustLevel = DeviceTrustLevel(false, false)), + ), + wasTrustedOnce = false + ) + } + } + } + + every { instance.getCrossSigningInfo(bobMxId) } answers { + + when (mode) { + StoreMode.Alice -> { + MXCrossSigningInfo( + bobMxId, + listOf( + bobMSKBase.copy(trustLevel = DeviceTrustLevel(false, false)), + bobUSKBase.copy(trustLevel = DeviceTrustLevel(false, false)), + ), + wasTrustedOnce = true + ) + } + StoreMode.Bob -> { + MXCrossSigningInfo( + bobMxId, + listOf( + bobMSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + bobUSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + bobSSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + ), + wasTrustedOnce = false + ) + } + } + } + + every { instance.getMyCrossSigningInfo() } answers { + when (mode) { + StoreMode.Alice -> MXCrossSigningInfo( + aliceMxId, + listOf( + aliceMSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + aliceUSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + aliceSSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + ), + wasTrustedOnce = false + ) + StoreMode.Bob -> MXCrossSigningInfo( + bobMxId, + listOf( + bobMSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + bobUSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + bobSSKBase.copy(trustLevel = DeviceTrustLevel(true, true)), + ), + wasTrustedOnce = false + ) + } + } + } + + companion object { + + val aliceMxId = "alice@example.com" + val bobMxId = "bob@example.com" + val bobDeviceId = "MKRJDSLYGA" + + private val aliceDevice1Id = "MGDAADVDMG" + + private val aliceMSK = "Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" + private val aliceSSK = "Rw6MiEn5do57mBWlWUvL6VDZJ7vAfGrTC58UXVyA0eo" + private val aliceUSK = "3XpDI8J5T1Wy2NoGePkDiVhqZlVeVPHM83q9sUJuRcc" + + private val bobMSK = "/ZK6paR+wBkKcazPx2xijn/0g+m2KCRqdCUZ6agzaaE" + private val bobSSK = "3/u3SRYywxRl2ul9OiRJK5zFeFnGXd0TrkcnVh1Bebk" + private val bobUSK = "601KhaiAhDTyFDS87leWc8/LB+EAUjKgjJvPMWNLP08" + + private val aliceFirstDevice = CryptoDeviceInfo( + deviceId = aliceDevice1Id, + userId = aliceMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$aliceDevice1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", + "ed25519:$aliceDevice1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", + ), + signatures = mapOf( + aliceMxId to mapOf( + "ed25519:$aliceDevice1Id" + to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", + "ed25519:$aliceMSK" + to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), + trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) + ) + + private val aBobDevice = CryptoDeviceInfo( + deviceId = bobDeviceId, + userId = bobMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", + "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", + ), + signatures = mapOf( + bobMxId to mapOf( + "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") + ) + + private val aliceMSKBase = CryptoCrossSigningKey( + userId = aliceMxId, + usages = listOf(KeyUsage.MASTER.value), + keys = mapOf( + "ed25519$aliceMSK" to aliceMSK + ), + trustLevel = DeviceTrustLevel(true, true), + signatures = emptyMap() + ) + + private val aliceSSKBase = CryptoCrossSigningKey( + userId = aliceMxId, + usages = listOf(KeyUsage.SELF_SIGNING.value), + keys = mapOf( + "ed25519$aliceSSK" to aliceSSK + ), + trustLevel = null, + signatures = emptyMap() + ) + + private val aliceUSKBase = CryptoCrossSigningKey( + userId = aliceMxId, + usages = listOf(KeyUsage.USER_SIGNING.value), + keys = mapOf( + "ed25519$aliceUSK" to aliceUSK + ), + trustLevel = null, + signatures = emptyMap() + ) + + val bobMSKBase = aliceMSKBase.copy( + userId = bobMxId, + keys = mapOf( + "ed25519$bobMSK" to bobMSK + ), + ) + val bobUSKBase = aliceMSKBase.copy( + userId = bobMxId, + keys = mapOf( + "ed25519$bobUSK" to bobUSK + ), + ) + val bobSSKBase = aliceMSKBase.copy( + userId = bobMxId, + keys = mapOf( + "ed25519$bobSSK" to bobSSK + ), + ) + } +} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt new file mode 100644 index 0000000000..f60741a3c7 --- /dev/null +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt @@ -0,0 +1,293 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification + +import android.util.Base64 +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.IVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady +import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent +import org.matrix.android.sdk.internal.crypto.SecretShareManager +import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.verification.FakeCryptoStoreForVerification +import org.matrix.android.sdk.internal.crypto.verification.StoreMode +import org.matrix.android.sdk.internal.crypto.verification.VerificationActor +import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo +import org.matrix.android.sdk.internal.crypto.verification.VerificationIntent +import org.matrix.android.sdk.internal.crypto.verification.VerificationTransportLayer +import org.matrix.android.sdk.internal.util.time.Clock +import java.util.UUID + +@OptIn(ExperimentalCoroutinesApi::class) +class VerificationActorTest { + + val transportScope = CoroutineScope(SupervisorJob()) + val actorAScope = CoroutineScope(SupervisorJob()) + val actorBScope = CoroutineScope(SupervisorJob()) + + @Before + fun setUp() { + // QR code needs that + mockkStatic(Base64::class) + every { + Base64.encodeToString(any(), any()) + } answers { + val array = firstArg() + java.util.Base64.getEncoder().encodeToString(array) + } + + every { + Base64.decode(any(), any()) + } answers { + val array = firstArg() + java.util.Base64.getDecoder().decode(array) + } + } + + @Test + fun `Request and accept`() = runTest { + var bobChannel: SendChannel? = null + var aliceChannel: SendChannel? = null + + val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel } + val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel } + + val aliceActor = fakeActor( + actorAScope, + FakeCryptoStoreForVerification.aliceMxId, + FakeCryptoStoreForVerification(StoreMode.Alice).instance, + aliceTransportLayer, + mockk> { + every { + get() + } returns mockk(relaxed = true) + } + ) + aliceChannel = aliceActor.channel + + val bobActor = fakeActor( + actorBScope, + FakeCryptoStoreForVerification.aliceMxId, + FakeCryptoStoreForVerification(StoreMode.Alice).instance, + bobTransportLayer, + mockk> { + every { + get() + } returns mockk(relaxed = true) + } + ) + bobChannel = bobActor.channel + + val completableDeferred = CompletableDeferred() + + transportScope.launch { + bobActor.eventFlow.collect { + if (it is VerificationEvent.RequestAdded) { + completableDeferred.complete(it.request) + return@collect cancel() + } + } + } + + awaitDeferrable { + aliceActor.send( + VerificationIntent.ActionRequestVerification( + otherUserId = FakeCryptoStoreForVerification.bobMxId, + roomId = "aRoom", + methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN), + deferred = it + ) + ) + } + + val bobIncomingRequest = completableDeferred.await() + bobIncomingRequest.state shouldBeEqualTo EVerificationState.Requested + + val aliceReadied = CompletableDeferred() + + val theJob = transportScope.launch { + aliceActor.eventFlow.collect { + if (it is VerificationEvent.RequestUpdated && it.request.state == EVerificationState.Ready) { + aliceReadied.complete(it.request) + return@collect cancel() + } + } + } + + // test ready + awaitDeferrable { + bobActor.send( + VerificationIntent.ActionReadyRequest( + bobIncomingRequest.transactionId, + methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SHOW, VerificationMethod.QR_CODE_SCAN), + it + ) + ) + } + + val readiedAliceSide = aliceReadied.await() + + println("transporte scope active? ${transportScope.isActive}") + println("the job? ${theJob.isActive}") + + readiedAliceSide.isSasSupported shouldBeEqualTo true + readiedAliceSide.otherCanScanQrCode shouldBeEqualTo true + } + + private suspend fun awaitDeferrable(block: suspend ((CompletableDeferred) -> Unit)): T { + val deferred = CompletableDeferred() + block.invoke(deferred) + return deferred.await() + } + + private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel?)): VerificationTransportLayer { + return mockk { + coEvery { sendToOther(any(), any(), any()) } answers { + val request = firstArg() + val type = secondArg() + val info = thirdArg>() + + transportScope.launch(Dispatchers.IO) { + when (type) { + EventType.KEY_VERIFICATION_READY -> { + val readyContent = info.asValidObject() + otherChannel()?.send( + VerificationIntent.OnReadyReceived( + transactionId = request.requestId(), + fromUser = fromUser, + viaRoom = request.roomId(), + readyInfo = readyContent as ValidVerificationInfoReady, + ) + ) + } + } + } + } + coEvery { sendInRoom(any(), any(), any(), any()) } answers { + val type = secondArg() + val roomId = thirdArg() + val content = arg(3) + + val fakeEventId = UUID.randomUUID().toString() + transportScope.launch(Dispatchers.IO) { + when (type) { + EventType.MESSAGE -> { + val requestContent = content.toModel()?.copy( + transactionId = fakeEventId + )?.asValidObject() + otherChannel()?.send( + VerificationIntent.OnVerificationRequestReceived( + requestContent!!, + senderId = FakeCryptoStoreForVerification.aliceMxId, + roomId = roomId, + timeStamp = 0 + ) + ) + } + EventType.KEY_VERIFICATION_READY -> { + val readyContent = content.toModel() + ?.asValidObject() + otherChannel()?.send( + VerificationIntent.OnReadyReceived( + transactionId = readyContent!!.transactionId, + fromUser = fromUser, + viaRoom = roomId, + readyInfo = readyContent, + ) + ) + } + } + } + fakeEventId + } + } + } + + @Test + fun `Every testing`() { + val mockStore = mockk() + every { mockStore.getDeviceId() } returns "A" + println("every ${mockStore.getDeviceId()}") + every { mockStore.getDeviceId() } returns "B" + println("every ${mockStore.getDeviceId()}") + + every { mockStore.getDeviceId() } returns "A" + every { mockStore.getDeviceId() } returns "B" + println("every ${mockStore.getDeviceId()}") + + every { mockStore.getCrossSigningInfo(any()) } returns null + every { mockStore.getCrossSigningInfo("alice") } returns MXCrossSigningInfo("alice", emptyList(), false) + + println("XS ${mockStore.getCrossSigningInfo("alice")}") + println("XS ${mockStore.getCrossSigningInfo("bob")}") + } + + private fun fakeActor( + scope: CoroutineScope, + userId: String, + cryptoStore: IMXCryptoStore, + transportLayer: VerificationTransportLayer, + crossSigningService: dagger.Lazy, + ): VerificationActor { + return VerificationActor( + scope, +// channel = channel, + clock = mockk { + every { epochMillis() } returns System.currentTimeMillis() + }, + myUserId = userId, + cryptoStore = cryptoStore, + secretShareManager = mockk {}, + transportLayer = transportLayer, + crossSigningService = crossSigningService, + setDeviceVerificationAction = SetDeviceVerificationAction( + cryptoStore = cryptoStore, + userId = userId, + defaultKeysBackupService = mockk { + coEvery { checkAndStartKeysBackup() } coAnswers { } + } + ) + ) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt index e9a5365b1c..66ec16ad08 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt @@ -16,17 +16,17 @@ package im.vector.app.test.fakes -import io.mockk.every +import io.mockk.coEvery import io.mockk.mockk import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService class FakeCrossSigningService : CrossSigningService by mockk() { fun givenIsCrossSigningInitializedReturns(isInitialized: Boolean) { - every { isCrossSigningInitialized() } returns isInitialized + coEvery { isCrossSigningInitialized() } returns isInitialized } fun givenIsCrossSigningVerifiedReturns(isVerified: Boolean) { - every { isCrossSigningVerified() } returns isVerified + coEvery { isCrossSigningVerified() } returns isVerified } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 0813728d4b..0d9b5b07e0 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -17,6 +17,7 @@ package im.vector.app.test.fakes import androidx.lifecycle.MutableLiveData +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -55,22 +56,19 @@ class FakeCryptoService( override fun getMyDevicesInfoLive(deviceId: String) = myDevicesInfoWithIdLiveData fun givenSetDeviceNameSucceeds() { - val matrixCallback = slot>() - every { setDeviceName(any(), any(), capture(matrixCallback)) } answers { - thirdArg>().onSuccess(Unit) + coEvery { setDeviceName(any(), any()) } answers { + Unit } } fun givenSetDeviceNameFailsWithError(error: Exception) { - val matrixCallback = slot>() - every { setDeviceName(any(), any(), capture(matrixCallback)) } answers { - thirdArg>().onFailure(error) + coEvery { setDeviceName(any(), any()) } answers { + throw error } } fun givenDeleteDeviceSucceeds(deviceId: String) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { + coEvery { deleteDevice(deviceId, any()) } answers { thirdArg>().onSuccess(Unit) } }