Actor unit test setup
This commit is contained in:
parent
5c82bdba38
commit
0c1e439313
|
@ -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
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ object CredentialsFixture {
|
|||
accessToken,
|
||||
refreshToken,
|
||||
homeServer,
|
||||
deviceId,
|
||||
deviceId ?: "",
|
||||
discoveryInformation,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 { }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue