Actor unit test setup

This commit is contained in:
Valere 2022-11-19 00:25:08 +01:00
parent 5c82bdba38
commit 0c1e439313
8 changed files with 792 additions and 144 deletions

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
@ -89,13 +88,7 @@ internal class DefaultVerificationService @Inject constructor(
private val stateMachine: VerificationActor private val stateMachine: VerificationActor
init { init {
val channel = Channel<VerificationIntent>( stateMachine = verificationActorFactory.create(executorScope)
capacity = Channel.UNLIMITED,
)
stateMachine = verificationActorFactory.create(channel)
executorScope.launch {
for (msg in channel) stateMachine.onReceive(msg)
}
} }
// It's obselete but not deprecated // It's obselete but not deprecated
// It's ok as it will be replaced by rust implementation // It's ok as it will be replaced by rust implementation

View file

@ -16,11 +16,15 @@
package org.matrix.android.sdk.internal.crypto.verification package org.matrix.android.sdk.internal.crypto.verification
import androidx.annotation.VisibleForTesting
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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.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.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.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest 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.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction 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.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.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho 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.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.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent 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.VERIFICATION_METHOD_SAS
import org.matrix.android.sdk.internal.crypto.model.rest.toValue 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.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.tools.withOlmUtility
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData 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.generateSharedSecretV2
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData
import org.matrix.android.sdk.internal.di.UserId 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 org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
import java.util.Locale import java.util.Locale
@ -91,21 +87,34 @@ import java.util.Locale
private val loggerTag = LoggerTag("Verification", LoggerTag.CRYPTO) private val loggerTag = LoggerTag("Verification", LoggerTag.CRYPTO)
internal class VerificationActor @AssistedInject constructor( internal class VerificationActor @AssistedInject constructor(
@Assisted private val channel: Channel<VerificationIntent>, @Assisted private val scope: CoroutineScope,
private val clock: Clock, private val clock: Clock,
@UserId private val myUserId: String, @UserId private val myUserId: String,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val sendVerificationMessageTask: SendVerificationMessageTask,
private val localEchoEventFactory: LocalEchoEventFactory,
private val sendToDeviceTask: SendToDeviceTask,
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: dagger.Lazy<CrossSigningService>, private val crossSigningService: dagger.Lazy<CrossSigningService>,
private val secretShareManager: SecretShareManager, private val secretShareManager: SecretShareManager,
private val transportLayer: VerificationTransportLayer,
) { ) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(channel: Channel<VerificationIntent>): VerificationActor fun create(scope: CoroutineScope): VerificationActor
}
@VisibleForTesting
val channel = Channel<VerificationIntent>(
capacity = Channel.UNLIMITED,
)
init {
scope.launch {
Timber.e("VALR BEFORE")
for (msg in channel) {
onReceive(msg)
}
Timber.e("VALR NNNNNNNN")
}
} }
// map [sender : [transaction]] // map [sender : [transaction]]
@ -121,7 +130,10 @@ internal class VerificationActor @AssistedInject constructor(
*/ */
private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>() private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>()
val eventFlow = MutableSharedFlow<VerificationEvent>(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<VerificationEvent>(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST)
suspend fun send(intent: VerificationIntent) { suspend fun send(intent: VerificationIntent) {
channel.send(intent) channel.send(intent)
@ -230,7 +242,7 @@ internal class VerificationActor @AssistedInject constructor(
handleIncomingRequest(msg) handleIncomingRequest(msg)
} }
is VerificationIntent.ActionReadyRequest -> { is VerificationIntent.ActionReadyRequest -> {
handleReadyRequest(msg) handleActionReadyRequest(msg)
} }
is VerificationIntent.ActionStartSasVerification -> { is VerificationIntent.ActionStartSasVerification -> {
handleSasStart(msg) handleSasStart(msg)
@ -323,9 +335,9 @@ internal class VerificationActor @AssistedInject constructor(
if (existingTx != null) { if (existingTx != null) {
existingTx.state = SasTransactionState.Cancelled(cancelCode, false) existingTx.state = SasTransactionState.Cancelled(cancelCode, false)
txMap[msg.fromUser]?.remove(msg.validCancel.transactionId) 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 -> { 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) { private suspend fun handleIncomingRequest(msg: VerificationIntent.OnVerificationRequestReceived) {
val pendingVerificationRequest = KotlinVerificationRequest( val pendingVerificationRequest = KotlinVerificationRequest(
requestId = msg.validRequestInfo.transactionId, requestId = msg.validRequestInfo.transactionId,
@ -395,7 +413,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
} }
matchingRequest.state = EVerificationState.Started matchingRequest.state = EVerificationState.Started
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
} }
private suspend fun handleReceiveStartForQR(request: KotlinVerificationRequest, reciprocate: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { 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?) // cancel if network error (would not send back a cancel but at least current user will see feedback?)
try { try {
sendToOther(request, EventType.KEY_VERIFICATION_ACCEPT, accept) transportLayer.sendToOther(request, EventType.KEY_VERIFICATION_ACCEPT, accept)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}] Failed to send accept for ${request.requestId}") .v("[${myUserId.take(8)}] Failed to send accept for ${request.requestId}")
@ -569,7 +587,7 @@ internal class VerificationActor @AssistedInject constructor(
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]: Sending my key $pubKey") .v("[${myUserId.take(8)}]: Sending my key $pubKey")
} }
sendToOther( transportLayer.sendToOther(
matchingRequest, matchingRequest,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
keyMessage, keyMessage,
@ -578,13 +596,13 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true)
matchingRequest.cancelCode = CancelCode.UserError matchingRequest.cancelCode = CancelCode.UserError
matchingRequest.state = EVerificationState.Cancelled matchingRequest.state = EVerificationState.Cancelled
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
return return
} }
existing.accepted = accept existing.accepted = accept
existing.state = SasTransactionState.SasKeySent existing.state = SasTransactionState.SasKeySent
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
} }
private suspend fun handleSasStart(msg: VerificationIntent.ActionStartSasVerification) { private suspend fun handleSasStart(msg: VerificationIntent.ActionStartSasVerification) {
@ -617,7 +635,7 @@ internal class VerificationActor @AssistedInject constructor(
requestId = msg.requestId requestId = msg.requestId
) )
sendToOther( transportLayer.sendToOther(
matchingRequest, matchingRequest,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
startMessage, startMessage,
@ -643,7 +661,7 @@ internal class VerificationActor @AssistedInject constructor(
) )
matchingRequest.state = EVerificationState.WeStarted matchingRequest.state = EVerificationState.WeStarted
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
addTransaction(tx) addTransaction(tx)
msg.deferred.complete(tx) msg.deferred.complete(tx)
@ -805,7 +823,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
try { try {
sendToOther(matchingRequest, EventType.KEY_VERIFICATION_START, message) transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_START, message)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.d("[${myUserId.take(8)}] Failed to send reciprocate message") .d("[${myUserId.take(8)}] Failed to send reciprocate message")
@ -814,7 +832,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
matchingRequest.state = EVerificationState.WeStarted matchingRequest.state = EVerificationState.WeStarted
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
val tx = KotlinQRVerification( val tx = KotlinQRVerification(
channel = this.channel, channel = this.channel,
@ -881,7 +899,7 @@ internal class VerificationActor @AssistedInject constructor(
val pubKey = existing.getSAS().publicKey val pubKey = existing.getSAS().publicKey
val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey) val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey)
try { try {
sendToOther( transportLayer.sendToOther(
matchingRequest, matchingRequest,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
keyMessage, keyMessage,
@ -898,13 +916,13 @@ internal class VerificationActor @AssistedInject constructor(
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]:i EMOJI CODE ${existing.getEmojiCodeRepresentation().joinToString(" ") { it.emoji }}") .v("[${myUserId.take(8)}]:i EMOJI CODE ${existing.getEmojiCodeRepresentation().joinToString(" ") { it.emoji }}")
} }
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
} catch (failure: Throwable) { } catch (failure: Throwable) {
existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true) existing.state = SasTransactionState.Cancelled(CancelCode.UserError, true)
matchingRequest.state = EVerificationState.Cancelled matchingRequest.state = EVerificationState.Cancelled
matchingRequest.cancelCode = CancelCode.UserError matchingRequest.cancelCode = CancelCode.UserError
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
return return
} }
} else { } else {
@ -931,7 +949,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
existing.calculateSASBytes(otherKey) existing.calculateSASBytes(otherKey)
existing.state = SasTransactionState.SasShortCodeReady existing.state = SasTransactionState.SasShortCodeReady
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
if (BuildConfig.LOG_PRIVATE_DATA) { if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]:o CODE ${existing.getDecimalCodeRepresentation()}") .v("[${myUserId.take(8)}]:o CODE ${existing.getDecimalCodeRepresentation()}")
@ -966,7 +984,7 @@ internal class VerificationActor @AssistedInject constructor(
// I can start verify, store it // I can start verify, store it
existing.theirMac = msg.validMac existing.theirMac = msg.validMac
existing.state = SasTransactionState.SasMacReceived(false) existing.state = SasTransactionState.SasMacReceived(false)
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
} }
else -> { else -> {
// it's a wrong state should cancel? // it's a wrong state should cancel?
@ -1026,12 +1044,12 @@ internal class VerificationActor @AssistedInject constructor(
if (isCorrectState) { if (isCorrectState) {
existing.state = SasTransactionState.Done(true) existing.state = SasTransactionState.Done(true)
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it // we can forget about it
txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId)
// XXX whatabout waiting for done? // XXX whatabout waiting for done?
matchingRequest.state = EVerificationState.Done matchingRequest.state = EVerificationState.Done
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
} else { } else {
// TODO cancel? // TODO cancel?
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
@ -1048,7 +1066,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
QRCodeVerificationState.WaitingForOtherDone -> { QRCodeVerificationState.WaitingForOtherDone -> {
matchingRequest.state = EVerificationState.Done matchingRequest.state = EVerificationState.Done
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
} }
else -> { else -> {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
@ -1101,7 +1119,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
} }
sendToOther( transportLayer.sendToOther(
matchingRequest, matchingRequest,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
if (matchingRequest.roomId != null) { if (matchingRequest.roomId != null) {
@ -1117,11 +1135,11 @@ internal class VerificationActor @AssistedInject constructor(
) )
existing.state = QRCodeVerificationState.Done existing.state = QRCodeVerificationState.Done
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it // we can forget about it
txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId) txMap[matchingRequest.otherUserId()]?.remove(matchingRequest.requestId)
matchingRequest.state = EVerificationState.WaitingForDone matchingRequest.state = EVerificationState.WaitingForDone
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
if (shouldRequestSecret) { if (shouldRequestSecret) {
matchingRequest.otherDeviceId()?.let { otherDeviceId -> matchingRequest.otherDeviceId()?.let { otherDeviceId ->
@ -1167,7 +1185,7 @@ internal class VerificationActor @AssistedInject constructor(
val macMsg = KotlinSasTransaction.sasMacMessage(matchingRequest.roomId != null, transactionId, macInfo) val macMsg = KotlinSasTransaction.sasMacMessage(matchingRequest.roomId != null, transactionId, macInfo)
try { try {
sendToOther(matchingRequest, EventType.KEY_VERIFICATION_MAC, macMsg) transportLayer.sendToOther(matchingRequest, EventType.KEY_VERIFICATION_MAC, macMsg)
} catch (failure: Throwable) { } catch (failure: Throwable) {
// it's a network problem, we don't need to cancel, user can retry? // it's a network problem, we don't need to cancel, user can retry?
msg.deferred.completeExceptionally(failure) msg.deferred.completeExceptionally(failure)
@ -1180,7 +1198,7 @@ internal class VerificationActor @AssistedInject constructor(
finalizeSasTransaction(existing, theirMac, matchingRequest, transactionId) finalizeSasTransaction(existing, theirMac, matchingRequest, transactionId)
} else { } else {
existing.state = SasTransactionState.SasMacSent existing.state = SasTransactionState.SasMacSent
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
} }
msg.deferred.complete(Unit) msg.deferred.complete(Unit)
@ -1237,7 +1255,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
// we should send done and wait for done // we should send done and wait for done
sendToOther( transportLayer.sendToOther(
matchingRequest, matchingRequest,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
if (matchingRequest.roomId != null) { if (matchingRequest.roomId != null) {
@ -1253,11 +1271,11 @@ internal class VerificationActor @AssistedInject constructor(
) )
existing.state = SasTransactionState.Done(false) existing.state = SasTransactionState.Done(false)
eventFlow.emit(VerificationEvent.TransactionUpdated(existing)) dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = existing pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = existing
txMap[matchingRequest.otherUserId]?.remove(transactionId) txMap[matchingRequest.otherUserId]?.remove(transactionId)
matchingRequest.state = EVerificationState.WaitingForDone matchingRequest.state = EVerificationState.WaitingForDone
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
} }
KotlinSasTransaction.MacVerificationResult.MismatchKeys, KotlinSasTransaction.MacVerificationResult.MismatchKeys,
KotlinSasTransaction.MacVerificationResult.MismatchMacCrossSigning, 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 val existing = pendingRequests
.flatMap { it.value } .flatMap { it.value }
.firstOrNull { it.requestId == msg.transactionId } .firstOrNull { it.requestId == msg.transactionId }
@ -1317,7 +1335,7 @@ internal class VerificationActor @AssistedInject constructor(
fromDevice = cryptoStore.getDeviceId() fromDevice = cryptoStore.getDeviceId()
) )
try { try {
sendToOther(existing, EventType.KEY_VERIFICATION_READY, message) transportLayer.sendToOther(existing, EventType.KEY_VERIFICATION_READY, message)
} catch (failure: Throwable) { } catch (failure: Throwable) {
msg.deferred.completeExceptionally(failure) msg.deferred.completeExceptionally(failure)
return return
@ -1326,7 +1344,9 @@ internal class VerificationActor @AssistedInject constructor(
existing.readyInfo = readyInfo existing.readyInfo = readyInfo
existing.qrCodeData = qrCodeData existing.qrCodeData = qrCodeData
existing.state = EVerificationState.Ready 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") Timber.tag(loggerTag.value).v("Request ${msg.transactionId} updated $existing")
msg.deferred.complete(existing.toPendingVerificationRequest()) msg.deferred.complete(existing.toPendingVerificationRequest())
@ -1424,13 +1444,11 @@ internal class VerificationActor @AssistedInject constructor(
timestamp = validInfo.timestamp, timestamp = validInfo.timestamp,
methods = validInfo.methods methods = validInfo.methods
) )
val event = createEventAndLocalEcho( val eventId = transportLayer.sendInRoom(
localId = validLocalId,
type = EventType.MESSAGE, type = EventType.MESSAGE,
roomId = msg.roomId, roomId = msg.roomId,
content = info.toContent() content = info.toContent()
) )
val eventId = sendEventInRoom(event)
val request = KotlinVerificationRequest( val request = KotlinVerificationRequest(
requestId = eventId, requestId = eventId,
incoming = false, incoming = false,
@ -1443,10 +1461,10 @@ internal class VerificationActor @AssistedInject constructor(
} }
requestsForUser.add(request) requestsForUser.add(request)
msg.deferred.complete(request.toPendingVerificationRequest()) msg.deferred.complete(request.toPendingVerificationRequest())
eventFlow.emit(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest()))
} else { } else {
val requestId = LocalEcho.createLocalEchoId() val requestId = LocalEcho.createLocalEchoId()
sendToDeviceEvent( transportLayer.sendToDeviceEvent(
messageType = EventType.KEY_VERIFICATION_REQUEST, messageType = EventType.KEY_VERIFICATION_REQUEST,
toSendToDeviceObject = KeyVerificationRequest( toSendToDeviceObject = KeyVerificationRequest(
transactionId = requestId, transactionId = requestId,
@ -1470,7 +1488,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
requestsForUser.add(request) requestsForUser.add(request)
msg.deferred.complete(request.toPendingVerificationRequest()) msg.deferred.complete(request.toPendingVerificationRequest())
eventFlow.emit(VerificationEvent.RequestAdded(request.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest()))
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
// some network problem // some network problem
@ -1499,13 +1517,13 @@ internal class VerificationActor @AssistedInject constructor(
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive")
matchingRequest.state = EVerificationState.HandledByOtherSession matchingRequest.state = EVerificationState.HandledByOtherSession
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
return return
} }
matchingRequest.readyInfo = msg.readyInfo matchingRequest.readyInfo = msg.readyInfo
matchingRequest.state = EVerificationState.Ready matchingRequest.state = EVerificationState.Ready
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
// if (matchingRequest.readyInfo != null) { // if (matchingRequest.readyInfo != null) {
// // TODO we already received a ready, cancel? or ignore // // TODO we already received a ready, cancel? or ignore
@ -1530,7 +1548,7 @@ internal class VerificationActor @AssistedInject constructor(
.orEmpty() .orEmpty()
try { try {
sendToDeviceEvent( transportLayer.sendToDeviceEvent(
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
KeyVerificationCancel( KeyVerificationCancel(
msg.transactionId, msg.transactionId,
@ -1556,7 +1574,7 @@ internal class VerificationActor @AssistedInject constructor(
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]: ready from another of my devices, make inactive") .v("[${myUserId.take(8)}]: ready from another of my devices, make inactive")
matchingRequest.state = EVerificationState.HandledByOtherSession matchingRequest.state = EVerificationState.HandledByOtherSession
eventFlow.emit(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
return return
} }
@ -1570,12 +1588,12 @@ internal class VerificationActor @AssistedInject constructor(
// requestsForUser.removeAt(index) // requestsForUser.removeAt(index)
// } // }
// requestsForUser.add(updated) // requestsForUser.add(updated)
// eventFlow.emit(VerificationEvent.RequestUpdated(updated)) // dispatchUpdate(VerificationEvent.RequestUpdated(updated))
// } // }
private suspend fun dispatchRequestAdded(tx: KotlinVerificationRequest) { private suspend fun dispatchRequestAdded(tx: KotlinVerificationRequest) {
Timber.v("## SAS dispatchRequestAdded txId:${tx.requestId}") Timber.v("## SAS dispatchRequestAdded txId:${tx.requestId}")
eventFlow.emit(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest()))
} }
// Utilities // Utilities
@ -1655,77 +1673,77 @@ internal class VerificationActor @AssistedInject constructor(
) )
} }
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { // private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
return Event( // return Event(
roomId = roomId, // roomId = roomId,
originServerTs = clock.epochMillis(), // originServerTs = clock.epochMillis(),
senderId = myUserId, // senderId = myUserId,
eventId = localId, // eventId = localId,
type = type, // type = type,
content = content, // content = content,
unsignedData = UnsignedData(age = null, transactionId = localId) // unsignedData = UnsignedData(age = null, transactionId = localId)
).also { // ).also {
localEchoEventFactory.createLocalEcho(it) // localEchoEventFactory.createLocalEcho(it)
} // }
} // }
//
private suspend fun sendEventInRoom(event: Event): String { // private suspend fun sendEventInRoom(event: Event): String {
return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId // return sendVerificationMessageTask.execute(SendVerificationMessageTask.Params(event, 5)).eventId
} // }
//
private suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List<String>) { // private suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List<String>) {
// TODO currently to device verification messages are sent unencrypted // // TODO currently to device verification messages are sent unencrypted
// as per spec not recommended // // as per spec not recommended
// > verification messages may be sent unencrypted, though this is not encouraged. // // > verification messages may be sent unencrypted, though this is not encouraged.
//
val contentMap = MXUsersDevicesMap<Any>() // val contentMap = MXUsersDevicesMap<Any>()
//
targetDevices.forEach { // targetDevices.forEach {
contentMap.setObject(otherUserId, it, toSendToDeviceObject) // contentMap.setObject(otherUserId, it, toSendToDeviceObject)
} // }
//
sendToDeviceTask // sendToDeviceTask
.execute(SendToDeviceTask.Params(messageType, contentMap)) // .execute(SendToDeviceTask.Params(messageType, contentMap))
} // }
//
suspend fun sendToOther( // suspend fun sendToOther(
request: KotlinVerificationRequest, // request: KotlinVerificationRequest,
type: String, // type: String,
verificationInfo: VerificationInfo<*>, // verificationInfo: VerificationInfo<*>,
) { // ) {
val roomId = request.roomId // val roomId = request.roomId
if (roomId != null) { // if (roomId != null) {
val event = createEventAndLocalEcho( // val event = createEventAndLocalEcho(
type = type, // type = type,
roomId = roomId, // roomId = roomId,
content = verificationInfo.toEventContent()!! // content = verificationInfo.toEventContent()!!
) // )
sendEventInRoom(event) // sendEventInRoom(event)
} else { // } else {
sendToDeviceEvent( // sendToDeviceEvent(
type, // type,
verificationInfo.toSendToDeviceObject()!!, // verificationInfo.toSendToDeviceObject()!!,
request.otherUserId, // request.otherUserId,
request.otherDeviceId()?.let { listOf(it) }.orEmpty() // request.otherDeviceId()?.let { listOf(it) }.orEmpty()
) // )
} // }
} // }
private suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) { private suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) {
request.state = EVerificationState.Cancelled request.state = EVerificationState.Cancelled
request.cancelCode = code request.cancelCode = code
eventFlow.emit(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest())) dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest()))
// should also update SAS/QR transaction // should also update SAS/QR transaction
getExistingTransaction<KotlinSasTransaction>(request.otherUserId, request.requestId)?.let { getExistingTransaction<KotlinSasTransaction>(request.otherUserId, request.requestId)?.let {
it.state = SasTransactionState.Cancelled(code, true) it.state = SasTransactionState.Cancelled(code, true)
txMap[request.otherUserId]?.remove(request.requestId) txMap[request.otherUserId]?.remove(request.requestId)
eventFlow.emit(VerificationEvent.TransactionUpdated(it)) dispatchUpdate(VerificationEvent.TransactionUpdated(it))
} }
getExistingTransaction<KotlinQRVerification>(request.otherUserId, request.requestId)?.let { getExistingTransaction<KotlinQRVerification>(request.otherUserId, request.requestId)?.let {
it.state = QRCodeVerificationState.Cancelled it.state = QRCodeVerificationState.Cancelled
txMap[request.otherUserId]?.remove(request.requestId) 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) 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) { private suspend fun cancelTransactionToDevice(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code") Timber.d("## SAS canceling transaction $transactionId for reason $code")
val cancelMessage = KeyVerificationCancel.create(transactionId, code) val cancelMessage = KeyVerificationCancel.create(transactionId, code)
val contentMap = MXUsersDevicesMap<Any>() // val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) // contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
sendToDeviceTask transportLayer.sendToDeviceEvent(
.execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) 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) { private suspend fun cancelTransactionInRoom(roomId: String, transactionId: String, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code") Timber.d("## SAS canceling transaction $transactionId for reason $code")
val cancelMessage = MessageVerificationCancelContent.create(transactionId, code) val cancelMessage = MessageVerificationCancelContent.create(transactionId, code)
val event = createEventAndLocalEcho( transportLayer.sendInRoom(
type = EventType.KEY_VERIFICATION_CANCEL, type = EventType.KEY_VERIFICATION_CANCEL,
roomId = roomId, roomId = roomId,
content = cancelMessage.toEventContent() content = cancelMessage.toEventContent()
) )
sendEventInRoom(event)
} }
private fun hashUsingAgreedHashMethod(hashMethod: String?, toHash: String): String { private fun hashUsingAgreedHashMethod(hashMethod: String?, toHash: String): String {
@ -1785,7 +1808,7 @@ internal class VerificationActor @AssistedInject constructor(
private suspend fun addTransaction(tx: VerificationTransaction) { private suspend fun addTransaction(tx: VerificationTransaction) {
val txInnerMap = txMap.getOrPut(tx.otherUserId) { mutableMapOf() } val txInnerMap = txMap.getOrPut(tx.otherUserId) { mutableMapOf() }
txInnerMap[tx.transactionId] = tx txInnerMap[tx.transactionId] = tx
eventFlow.emit(VerificationEvent.TransactionAdded(tx)) dispatchUpdate(VerificationEvent.TransactionAdded(tx))
} }
private inline fun <reified T : VerificationTransaction> getExistingTransaction(otherUserId: String, transactionId: String): T? { private inline fun <reified T : VerificationTransaction> getExistingTransaction(otherUserId: String, transactionId: String): T? {

View file

@ -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<String>) {
// 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<Any>()
targetDevices.forEach {
contentMap.setObject(otherUserId, it, toSendToDeviceObject)
}
sendToDeviceTask
.execute(SendToDeviceTask.Params(messageType, contentMap))
}
}

View file

@ -32,7 +32,7 @@ object CredentialsFixture {
accessToken, accessToken,
refreshToken, refreshToken,
homeServer, homeServer,
deviceId, deviceId ?: "",
discoveryInformation, discoveryInformation,
) )
} }

View file

@ -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<IMXCryptoStore>()
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
),
)
}
}

View file

@ -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<ByteArray>()
java.util.Base64.getEncoder().encodeToString(array)
}
every {
Base64.decode(any<String>(), any())
} answers {
val array = firstArg<String>()
java.util.Base64.getDecoder().decode(array)
}
}
@Test
fun `Request and accept`() = runTest {
var bobChannel: SendChannel<VerificationIntent>? = null
var aliceChannel: SendChannel<VerificationIntent>? = 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<dagger.Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
aliceChannel = aliceActor.channel
val bobActor = fakeActor(
actorBScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
bobTransportLayer,
mockk<dagger.Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
bobChannel = bobActor.channel
val completableDeferred = CompletableDeferred<PendingVerificationRequest>()
transportScope.launch {
bobActor.eventFlow.collect {
if (it is VerificationEvent.RequestAdded) {
completableDeferred.complete(it.request)
return@collect cancel()
}
}
}
awaitDeferrable<PendingVerificationRequest> {
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<PendingVerificationRequest>()
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<PendingVerificationRequest?> {
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 <T> awaitDeferrable(block: suspend ((CompletableDeferred<T>) -> Unit)): T {
val deferred = CompletableDeferred<T>()
block.invoke(deferred)
return deferred.await()
}
private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel<VerificationIntent>?)): VerificationTransportLayer {
return mockk<VerificationTransportLayer> {
coEvery { sendToOther(any(), any(), any()) } answers {
val request = firstArg<IVerificationRequest>()
val type = secondArg<String>()
val info = thirdArg<VerificationInfo<*>>()
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<String>()
val roomId = thirdArg<String>()
val content = arg<Content>(3)
val fakeEventId = UUID.randomUUID().toString()
transportScope.launch(Dispatchers.IO) {
when (type) {
EventType.MESSAGE -> {
val requestContent = content.toModel<MessageVerificationRequestContent>()?.copy(
transactionId = fakeEventId
)?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnVerificationRequestReceived(
requestContent!!,
senderId = FakeCryptoStoreForVerification.aliceMxId,
roomId = roomId,
timeStamp = 0
)
)
}
EventType.KEY_VERIFICATION_READY -> {
val readyContent = content.toModel<MessageVerificationReadyContent>()
?.asValidObject()
otherChannel()?.send(
VerificationIntent.OnReadyReceived(
transactionId = readyContent!!.transactionId,
fromUser = fromUser,
viaRoom = roomId,
readyInfo = readyContent,
)
)
}
}
}
fakeEventId
}
}
}
@Test
fun `Every testing`() {
val mockStore = mockk<IMXCryptoStore>()
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<CrossSigningService>,
): VerificationActor {
return VerificationActor(
scope,
// channel = channel,
clock = mockk<Clock> {
every { epochMillis() } returns System.currentTimeMillis()
},
myUserId = userId,
cryptoStore = cryptoStore,
secretShareManager = mockk<SecretShareManager> {},
transportLayer = transportLayer,
crossSigningService = crossSigningService,
setDeviceVerificationAction = SetDeviceVerificationAction(
cryptoStore = cryptoStore,
userId = userId,
defaultKeysBackupService = mockk {
coEvery { checkAndStartKeysBackup() } coAnswers { }
}
)
)
}
}

View file

@ -16,17 +16,17 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import io.mockk.every import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
class FakeCrossSigningService : CrossSigningService by mockk() { class FakeCrossSigningService : CrossSigningService by mockk() {
fun givenIsCrossSigningInitializedReturns(isInitialized: Boolean) { fun givenIsCrossSigningInitializedReturns(isInitialized: Boolean) {
every { isCrossSigningInitialized() } returns isInitialized coEvery { isCrossSigningInitialized() } returns isInitialized
} }
fun givenIsCrossSigningVerifiedReturns(isVerified: Boolean) { fun givenIsCrossSigningVerifiedReturns(isVerified: Boolean) {
every { isCrossSigningVerified() } returns isVerified coEvery { isCrossSigningVerified() } returns isVerified
} }
} }

View file

@ -17,6 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
@ -55,22 +56,19 @@ class FakeCryptoService(
override fun getMyDevicesInfoLive(deviceId: String) = myDevicesInfoWithIdLiveData override fun getMyDevicesInfoLive(deviceId: String) = myDevicesInfoWithIdLiveData
fun givenSetDeviceNameSucceeds() { fun givenSetDeviceNameSucceeds() {
val matrixCallback = slot<MatrixCallback<Unit>>() coEvery { setDeviceName(any(), any()) } answers {
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers { Unit
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
} }
} }
fun givenSetDeviceNameFailsWithError(error: Exception) { fun givenSetDeviceNameFailsWithError(error: Exception) {
val matrixCallback = slot<MatrixCallback<Unit>>() coEvery { setDeviceName(any(), any()) } answers {
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers { throw error
thirdArg<MatrixCallback<Unit>>().onFailure(error)
} }
} }
fun givenDeleteDeviceSucceeds(deviceId: String) { fun givenDeleteDeviceSucceeds(deviceId: String) {
val matrixCallback = slot<MatrixCallback<Unit>>() coEvery { deleteDevice(deviceId, any()) } answers {
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit) thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
} }
} }