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.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<VerificationIntent>(
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

View file

@ -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<VerificationIntent>,
@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<CrossSigningService>,
private val secretShareManager: SecretShareManager,
private val transportLayer: VerificationTransportLayer,
) {
@AssistedFactory
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]]
@ -121,7 +130,10 @@ internal class VerificationActor @AssistedInject constructor(
*/
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) {
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<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))
}
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<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))
// }
//
// 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<KotlinSasTransaction>(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<KotlinQRVerification>(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<Any>()
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
sendToDeviceTask
.execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap))
// val contentMap = MXUsersDevicesMap<Any>()
// 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 <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,
refreshToken,
homeServer,
deviceId,
deviceId ?: "",
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
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
}
}

View file

@ -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<MatrixCallback<Unit>>()
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
coEvery { setDeviceName(any(), any()) } answers {
Unit
}
}
fun givenSetDeviceNameFailsWithError(error: Exception) {
val matrixCallback = slot<MatrixCallback<Unit>>()
every { setDeviceName(any(), any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onFailure(error)
coEvery { setDeviceName(any(), any()) } answers {
throw error
}
}
fun givenDeleteDeviceSucceeds(deviceId: String) {
val matrixCallback = slot<MatrixCallback<Unit>>()
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
coEvery { deleteDevice(deviceId, any()) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
}
}