refactor for easy unit tests

This commit is contained in:
Valere 2022-11-21 15:16:34 +01:00
parent bed2c221e3
commit 4ce6a25c70
9 changed files with 725 additions and 436 deletions

View File

@ -62,7 +62,8 @@ internal class KotlinSasTransaction(
override val isIncoming: Boolean,
val startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null,
val isToDevice: Boolean,
var state: SasTransactionState
var state: SasTransactionState,
val olmSAS: OlmSAS,
) : SasVerificationTransaction {
override val method: VerificationMethod
@ -183,11 +184,8 @@ internal class KotlinSasTransaction(
}
}
private var olmSas: OlmSAS? = null
fun getSAS(): OlmSAS {
if (olmSas == null) olmSas = OlmSAS()
return olmSas!!
override fun toString(): String {
return "KotlinSasTransaction(transactionId=$transactionId, state=$state, otherUserId=$otherUserId, otherDeviceId=$otherDeviceId, isToDevice=$isToDevice)"
}
// To override finalize(), all you need to do is simply declare it, without using the override keyword:
@ -197,8 +195,7 @@ internal class KotlinSasTransaction(
private fun releaseSAS() {
// finalization logic
olmSas?.releaseSas()
olmSas = null
olmSAS.releaseSas()
}
var accepted: ValidVerificationInfoAccept? = null
@ -206,6 +203,7 @@ internal class KotlinSasTransaction(
var shortCodeBytes: ByteArray? = null
var myMac: ValidVerificationInfoMac? = null
var theirMac: ValidVerificationInfoMac? = null
var verifiedSuccessInfo: MacVerificationResult.Success? = null
override fun state() = this.state
@ -262,7 +260,7 @@ internal class KotlinSasTransaction(
fun calculateSASBytes(otherKey: String) {
this.otherKey = otherKey
getSAS().setTheirPublicKey(otherKey)
olmSAS.setTheirPublicKey(otherKey)
shortCodeBytes = when (accepted!!.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
@ -280,7 +278,7 @@ internal class KotlinSasTransaction(
append(otherDeviceId)
append(myUserId)
append(myDeviceId)
append(getSAS().publicKey)
append(olmSAS.publicKey)
} else {
append(myUserId)
append(myDeviceId)
@ -291,7 +289,7 @@ internal class KotlinSasTransaction(
}
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
getSAS().generateShortCode(sasInfo, 6)
olmSAS.generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
val sasInfo = buildString {
@ -302,18 +300,18 @@ internal class KotlinSasTransaction(
append(otherKey).append('|')
append(myUserId).append('|')
append(myDeviceId).append('|')
append(getSAS().publicKey).append('|')
append(olmSAS.publicKey).append('|')
} else {
append(myUserId).append('|')
append(myDeviceId).append('|')
append(getSAS().publicKey).append('|')
append(olmSAS.publicKey).append('|')
append(otherUserId).append('|')
append(otherDeviceId).append('|')
append(otherKey).append('|')
}
append(transactionId)
}
getSAS().generateShortCode(sasInfo, 6)
olmSAS.generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
@ -463,13 +461,16 @@ internal class KotlinSasTransaction(
return MacVerificationResult.Success(
verifiedDevices,
otherMasterKeyIsVerified
)
).also {
// store and will persist when transaction is actually done
verifiedSuccessInfo = it
}
}
private fun macUsingAgreedMethod(message: String, info: String): String? {
return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) {
SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
SAS_MAC_SHA256 -> getSAS().calculateMac(message, info)
SAS_MAC_SHA256_LONGKDF -> olmSAS.calculateMacLongKdf(message, info)
SAS_MAC_SHA256 -> olmSAS.calculateMac(message, info)
else -> null
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -29,15 +29,12 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
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
import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
@ -57,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest
@ -67,8 +63,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
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.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
@ -77,24 +71,17 @@ import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import java.util.Locale
// data class AddRequestActions(
// val request: PendingVerificationRequest,
// // only allow one active verification between two users
// // so if there are already active requests they should be canceled
// val toCancel: List<PendingVerificationRequest>
// )
private val loggerTag = LoggerTag("Verification", LoggerTag.CRYPTO)
internal class VerificationActor @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
private val clock: Clock,
@UserId private val myUserId: String,
private val cryptoStore: IMXCryptoStore,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: dagger.Lazy<CrossSigningService>,
private val secretShareManager: SecretShareManager,
private val transportLayer: VerificationTransportLayer,
private val verificationRequestsStore: VerificationRequestsStore,
private val olmPrimitiveProvider: VerificationCryptoPrimitiveProvider,
private val verificationTrustBackend: VerificationTrustBackend,
) {
@AssistedFactory
@ -109,31 +96,15 @@ internal class VerificationActor @AssistedInject constructor(
init {
scope.launch {
Timber.e("VALR BEFORE")
for (msg in channel) {
onReceive(msg)
}
Timber.e("VALR NNNNNNNN")
}
}
// map [sender : [transaction]]
private val txMap = HashMap<String, MutableMap<String, VerificationTransaction>>()
// we need to keep track of finished transaction
// It will be used for gossiping (to send request after request is completed and 'done' by other)
private val pastTransactions = HashMap<String, MutableMap<String, VerificationTransaction>>()
/**
* Map [sender: [PendingVerificationRequest]]
* For now we keep all requests (even terminated ones) during the lifetime of the app.
*/
private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>()
// Replaces the typical list of listeners pattern.
// Looks to me as the sane setup, not sure if more than 1 is needed as extraBufferCapacity
// 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)
val eventFlow = MutableSharedFlow<VerificationEvent>(extraBufferCapacity = 20, onBufferOverflow = BufferOverflow.SUSPEND)
suspend fun send(intent: VerificationIntent) {
channel.send(intent)
@ -144,8 +115,7 @@ internal class VerificationActor @AssistedInject constructor(
requestId: String,
block: suspend ((KotlinVerificationRequest) -> Unit)
) {
val matchingRequest = pendingRequests[otherUserId]
?.firstOrNull { it.requestId == requestId }
val matchingRequest = verificationRequestsStore.getExistingRequest(otherUserId, requestId)
?: return Unit.also {
// Receive a transaction event with no matching request.. should ignore.
// Not supported any more to do raw start
@ -173,8 +143,7 @@ internal class VerificationActor @AssistedInject constructor(
viaRoom: String?,
block: suspend ((KotlinVerificationRequest) -> Unit)
) {
val matchingRequest = pendingRequests[otherUserId]
?.firstOrNull { it.requestId == requestId }
val matchingRequest = verificationRequestsStore.getExistingRequest(otherUserId, requestId)
?: return Unit.also {
// Receive a transaction event with no matching request.. should ignore.
// Not supported any more to do raw start
@ -219,23 +188,11 @@ internal class VerificationActor @AssistedInject constructor(
is VerificationIntent.OnReadyReceived -> {
handleReadyReceived(msg)
}
is VerificationIntent.FailToSendRequest -> {
// just delete it?
val requestsForUser = pendingRequests.getOrPut(msg.request.otherUserId) { mutableListOf() }
val index = requestsForUser.indexOfFirst {
it.requestId == msg.request.transactionId
}
if (index != -1) {
requestsForUser.removeAt(index)
}
}
// is VerificationIntent.UpdateRequest -> {
// updatePendingRequest(msg.request)
// }
is VerificationIntent.GetExistingRequestInRoom -> {
val existing = pendingRequests.flatMap { entry ->
entry.value.filter { it.roomId == msg.roomId && it.requestId == msg.transactionId }
}.firstOrNull()
val existing = verificationRequestsStore.getExistingRequestInRoom(msg.transactionId, msg.roomId)
msg.deferred.complete(existing?.toPendingVerificationRequest())
}
is VerificationIntent.OnVerificationRequestReceived -> {
@ -286,9 +243,7 @@ internal class VerificationActor @AssistedInject constructor(
}
}
is VerificationIntent.ActionCancel -> {
pendingRequests
.flatMap { it.value }
.firstOrNull { it.requestId == msg.transactionId }
verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId)
?.let { matchingRequest ->
try {
cancelRequest(matchingRequest, CancelCode.User)
@ -300,25 +255,27 @@ internal class VerificationActor @AssistedInject constructor(
}
is VerificationIntent.OnUnableToDecryptVerificationEvent -> {
// at least if request was sent by me, I can safely cancel without interfering
val matchingRequest = pendingRequests[msg.fromUser]
?.firstOrNull { it.requestId == msg.transactionId } ?: return
val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId)
?: return
if (matchingRequest.state != EVerificationState.HandledByOtherSession) {
cancelRequest(matchingRequest, CancelCode.InvalidMessage)
}
}
is VerificationIntent.GetExistingRequestsForUser -> {
pendingRequests[msg.userId].orEmpty().let { requests ->
verificationRequestsStore.getExistingRequestsForUser(msg.userId).let { requests ->
msg.deferred.complete(requests.map { it.toPendingVerificationRequest() })
}
}
is VerificationIntent.GetExistingTransaction -> {
txMap[msg.fromUser]?.get(msg.transactionId)?.let {
verificationRequestsStore
.getExistingTransaction(msg.fromUser, msg.transactionId)
?.let {
msg.deferred.complete(it)
}
}
is VerificationIntent.GetExistingRequest -> {
pendingRequests[msg.otherUserId]
?.firstOrNull { msg.transactionId == it.requestId }
verificationRequestsStore
.getExistingRequest(msg.otherUserId, msg.transactionId)
?.let {
msg.deferred.complete(it.toPendingVerificationRequest())
}
@ -334,7 +291,7 @@ internal class VerificationActor @AssistedInject constructor(
getExistingTransaction(msg.validCancel.transactionId) // txMap[msg.fromUser]?.get(msg.validCancel.transactionId)
if (existingTx != null) {
existingTx.state = SasTransactionState.Cancelled(cancelCode, false)
txMap[msg.fromUser]?.remove(msg.validCancel.transactionId)
verificationRequestsStore.deleteTransaction(msg.fromUser, msg.validCancel.transactionId)
dispatchUpdate(VerificationEvent.TransactionUpdated(existingTx))
}
dispatchUpdate(VerificationEvent.RequestUpdated(request.toPendingVerificationRequest()))
@ -348,8 +305,10 @@ 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)
// If no subscriber there is a small buffer
scope.launch {
eventFlow.emit(update)
}
}
private suspend fun handleIncomingRequest(msg: VerificationIntent.OnVerificationRequestReceived) {
@ -363,15 +322,14 @@ internal class VerificationActor @AssistedInject constructor(
requestInfo = msg.validRequestInfo
roomId = msg.roomId
}
pendingRequests.getOrPut(msg.senderId) { mutableListOf() }
.add(pendingVerificationRequest)
verificationRequestsStore.addRequest(msg.senderId, pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest)
}
private suspend fun onStartReceived(msg: VerificationIntent.OnStartReceived) {
val requestId = msg.validVerificationInfoStart.transactionId
val matchingRequest = pendingRequests[msg.fromUser]?.firstOrNull { it.requestId == requestId }
val matchingRequest = verificationRequestsStore
.getExistingRequestWithRequestId(msg.validVerificationInfoStart.transactionId)
?: return Unit.also {
// Receive a start with no matching request.. should ignore.
// Not supported any more to do raw start
@ -493,7 +451,7 @@ internal class VerificationActor @AssistedInject constructor(
cancelRequest(request, CancelCode.UnknownMethod)
}
// Bobs device ensures that it has a copy of Alices device key.
val mxDeviceInfo = cryptoStore.getUserDevice(userId = request.otherUserId, deviceId = otherDeviceId)
val mxDeviceInfo = verificationTrustBackend.getUserDevice(request.otherUserId, otherDeviceId)
if (mxDeviceInfo?.fingerprint() == null) {
Timber.e("## SAS Failed to find device key ")
@ -509,19 +467,17 @@ internal class VerificationActor @AssistedInject constructor(
state = SasTransactionState.None,
otherUserId = request.otherUserId,
myUserId = myUserId,
myTrustedMSK = cryptoStore.getMyCrossSigningInfo()
?.takeIf { it.isTrusted() }
?.masterKey()
?.unpaddedBase64PublicKey,
myTrustedMSK = verificationTrustBackend.getMyTrustedMasterKeyBase64(),
otherDeviceId = request.otherDeviceId(),
myDeviceId = cryptoStore.getDeviceId(),
myDeviceFingerprint = cryptoStore.getUserDevice(myUserId, cryptoStore.getDeviceId())?.fingerprint().orEmpty(),
myDeviceId = verificationTrustBackend.getMyDeviceId(),
myDeviceFingerprint = verificationTrustBackend.getMyDevice().fingerprint().orEmpty(),
startReq = sasStart,
isIncoming = true,
isToDevice = msg.viaRoom == null,
olmSAS = olmPrimitiveProvider.provideOlmSas()
)
val concat = sasTx.getSAS().publicKey + sasStart.canonicalJson
val concat = sasTx.olmSAS.publicKey + sasStart.canonicalJson
val commitment = hashUsingAgreedHashMethod(agreedHash, concat)
val accept = KotlinSasTransaction.sasAccept(
@ -544,6 +500,7 @@ internal class VerificationActor @AssistedInject constructor(
}
sasTx.accepted = accept.asValidObject()
sasTx.state = SasTransactionState.SasAccepted
addTransaction(sasTx)
}
@ -578,7 +535,7 @@ internal class VerificationActor @AssistedInject constructor(
// Alices device creates an ephemeral Curve25519 key pair (dA,QA),
// and replies with a to_device message with type set to “m.key.verification.key”, sending Alices public key QA
val pubKey = existing.getSAS().publicKey
val pubKey = existing.olmSAS.publicKey
val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey)
@ -606,10 +563,7 @@ internal class VerificationActor @AssistedInject constructor(
}
private suspend fun handleSasStart(msg: VerificationIntent.ActionStartSasVerification) {
val matchingRequest = pendingRequests
.flatMap { entry ->
entry.value.filter { it.requestId == msg.requestId }
}.firstOrNull()
val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.requestId)
?: return Unit.also {
msg.deferred.completeExceptionally(java.lang.IllegalArgumentException("Unknown request"))
}
@ -631,7 +585,7 @@ internal class VerificationActor @AssistedInject constructor(
}
val startMessage = KotlinSasTransaction.sasStart(
inRoom = matchingRequest.roomId != null,
fromDevice = cryptoStore.getDeviceId(),
fromDevice = verificationTrustBackend.getMyDeviceId(),
requestId = msg.requestId
)
@ -648,16 +602,14 @@ internal class VerificationActor @AssistedInject constructor(
state = SasTransactionState.SasStarted,
otherUserId = msg.otherUserId,
myUserId = myUserId,
myTrustedMSK = cryptoStore.getMyCrossSigningInfo()
?.takeIf { it.isTrusted() }
?.masterKey()
?.unpaddedBase64PublicKey,
myTrustedMSK = verificationTrustBackend.getMyTrustedMasterKeyBase64(),
otherDeviceId = otherDeviceId,
myDeviceId = cryptoStore.getDeviceId(),
myDeviceFingerprint = cryptoStore.getUserDevice(myUserId, cryptoStore.getDeviceId())?.fingerprint().orEmpty(),
myDeviceId = verificationTrustBackend.getMyDeviceId(),
myDeviceFingerprint = verificationTrustBackend.getMyDevice().fingerprint().orEmpty(),
startReq = startMessage.asValidObject() as ValidVerificationInfoStart.SasVerificationInfoStart,
isIncoming = false,
isToDevice = matchingRequest.roomId == null
isToDevice = matchingRequest.roomId == null,
olmSAS = olmPrimitiveProvider.provideOlmSas()
)
matchingRequest.state = EVerificationState.WeStarted
@ -670,10 +622,7 @@ internal class VerificationActor @AssistedInject constructor(
private suspend fun handleActionReciprocateQR(msg: VerificationIntent.ActionReciprocateQrVerification) {
Timber.tag(loggerTag.value)
.d("[${myUserId.take(8)}] handle reciprocate for ${msg.requestId}")
val matchingRequest = pendingRequests
.flatMap { entry ->
entry.value.filter { it.requestId == msg.requestId }
}.firstOrNull()
val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.requestId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.d("[${myUserId.take(8)}] No matching request, abort ${msg.requestId}")
@ -700,8 +649,7 @@ internal class VerificationActor @AssistedInject constructor(
return
}
val myMasterKey = crossSigningService.get()
.getUserCrossSigningKeys(myUserId)?.masterKey()?.unpaddedBase64PublicKey
val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId)
// Check the other device view of my MSK
val otherQrCodeData = msg.scannedData.toQrCodeData()
@ -725,9 +673,7 @@ internal class VerificationActor @AssistedInject constructor(
return
}
val whatIThinkOtherMskIs = crossSigningService.get().getUserCrossSigningKeys(matchingRequest.otherUserId)
?.masterKey()
?.unpaddedBase64PublicKey
val whatIThinkOtherMskIs = verificationTrustBackend.getUserMasterKeyBase64(matchingRequest.otherUserId)
if (whatIThinkOtherMskIs != otherQrCodeData.userMasterCrossSigningPublicKey) {
Timber.tag(loggerTag.value)
.d("[${myUserId.take(8)}] ## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}")
@ -756,7 +702,7 @@ internal class VerificationActor @AssistedInject constructor(
return
}
val whatOtherThinkMyDeviceKeyIs = otherQrCodeData.otherDeviceKey
val myDeviceKey = cryptoStore.getUserDevice(myUserId, cryptoStore.getDeviceId())?.fingerprint()
val myDeviceKey = verificationTrustBackend.getMyDevice().fingerprint()
if (whatOtherThinkMyDeviceKeyIs != myDeviceKey) {
Timber.tag(loggerTag.value)
.d("[${myUserId.take(8)}] ## Verification QR: Invalid other device key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
@ -777,7 +723,7 @@ internal class VerificationActor @AssistedInject constructor(
// Let's check that it's the good one
// If not -> Cancel
val otherDeclaredDeviceKey = otherQrCodeData.deviceKey
val whatIThinkItIs = cryptoStore.getUserDevice(myUserId, otherDeviceId)?.fingerprint()
val whatIThinkItIs = verificationTrustBackend.getUserDevice(matchingRequest.otherUserId, otherDeviceId)?.fingerprint()
if (otherDeclaredDeviceKey != whatIThinkItIs) {
Timber.tag(loggerTag.value)
@ -802,7 +748,7 @@ internal class VerificationActor @AssistedInject constructor(
// qrCodeData.sharedSecret will be used to send the start request
val message = if (matchingRequest.roomId != null) {
MessageVerificationStartContent(
fromDevice = cryptoStore.getDeviceId(),
fromDevice = verificationTrustBackend.getMyDeviceId(),
hashes = null,
keyAgreementProtocols = null,
messageAuthenticationCodes = null,
@ -816,7 +762,7 @@ internal class VerificationActor @AssistedInject constructor(
)
} else {
KeyVerificationStart(
fromDevice = cryptoStore.getDeviceId(),
fromDevice = verificationTrustBackend.getMyDeviceId(),
sharedSecret = otherQrCodeData.sharedSecret,
method = VERIFICATION_METHOD_RECIPROCATE,
)
@ -896,7 +842,7 @@ internal class VerificationActor @AssistedInject constructor(
val otherKey = msg.validKey.key
if (existing.isIncoming) {
// ok i can now send my key and compute the sas code
val pubKey = existing.getSAS().publicKey
val pubKey = existing.olmSAS.publicKey
val keyMessage = KotlinSasTransaction.sasKeyMessage(matchingRequest.roomId != null, requestId, pubKey)
try {
transportLayer.sendToOther(
@ -945,7 +891,7 @@ internal class VerificationActor @AssistedInject constructor(
if (otherCommitment == existing.accepted?.commitment) {
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]:o calculate SAS my key ${existing.getSAS().publicKey} their Key: $otherKey")
.v("[${myUserId.take(8)}]:o calculate SAS my key ${existing.olmSAS.publicKey} their Key: $otherKey")
}
existing.calculateSASBytes(otherKey)
existing.state = SasTransactionState.SasShortCodeReady
@ -997,7 +943,7 @@ internal class VerificationActor @AssistedInject constructor(
private suspend fun handleSasCodeDoesNotMatch(msg: VerificationIntent.ActionSASCodeDoesNotMatch) {
val transactionId = msg.transactionId
val matchingRequest = pendingRequests.flatMap { it.value }.firstOrNull { it.requestId == transactionId }
val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId)
?: return Unit.also {
msg.deferred.completeExceptionally(IllegalStateException("Unknown Request"))
}
@ -1046,7 +992,7 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = SasTransactionState.Done(true)
dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it
txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId)
verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, matchingRequest.requestId)
// XXX whatabout waiting for done?
matchingRequest.state = EVerificationState.Done
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
@ -1090,7 +1036,7 @@ internal class VerificationActor @AssistedInject constructor(
// let's trust him
// it's his code scanned so user is him and other me
try {
crossSigningService.get().trustUser(matchingRequest.otherUserId)
verificationTrustBackend.trustUser(matchingRequest.otherUserId)
} catch (failure: Throwable) {
// fail silently?
// at least it will be marked as trusted locally?
@ -1103,7 +1049,7 @@ internal class VerificationActor @AssistedInject constructor(
// Also notify the secret share manager for the soon to come secret share requests
secretShareManager.onVerificationCompleteForDevice(matchingRequest.otherDeviceId()!!)
try {
crossSigningService.get().trustDevice(matchingRequest.otherDeviceId()!!)
verificationTrustBackend.trustOwnDevice(matchingRequest.otherDeviceId()!!)
} catch (failure: Throwable) {
// network problem??
Timber.w("## Verification: Failed to sign new device ${matchingRequest.otherDeviceId()}, ${failure.localizedMessage}")
@ -1111,7 +1057,7 @@ internal class VerificationActor @AssistedInject constructor(
}
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
// I can trust my MSK
crossSigningService.get().markMyMasterKeyAsTrusted()
verificationTrustBackend.markMyMasterKeyAsTrusted()
shouldRequestSecret = true
}
null -> {
@ -1137,7 +1083,7 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = QRCodeVerificationState.Done
dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
// we can forget about it
txMap[matchingRequest.otherUserId]?.remove(matchingRequest.requestId)
verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, matchingRequest.requestId)
matchingRequest.state = EVerificationState.WaitingForDone
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
@ -1153,7 +1099,7 @@ internal class VerificationActor @AssistedInject constructor(
private suspend fun handleSasCodeMatch(msg: VerificationIntent.ActionSASCodeMatches) {
val transactionId = msg.transactionId
val matchingRequest = pendingRequests.flatMap { it.value }.firstOrNull { it.requestId == transactionId }
val matchingRequest = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId)
?: return Unit.also {
msg.deferred.completeExceptionally(IllegalStateException("Unknown Request"))
}
@ -1212,8 +1158,8 @@ internal class VerificationActor @AssistedInject constructor(
) {
val result = existing.verifyMacs(
theirMac,
cryptoStore.getUserDeviceList(matchingRequest.otherUserId).orEmpty(),
cryptoStore.getCrossSigningInfo(matchingRequest.otherUserId)?.masterKey()?.unpaddedBase64PublicKey
verificationTrustBackend.getUserDeviceList(matchingRequest.otherUserId),
verificationTrustBackend.getUserMasterKeyBase64(matchingRequest.otherUserId)
)
Timber.tag(loggerTag.value)
@ -1222,20 +1168,13 @@ internal class VerificationActor @AssistedInject constructor(
is KotlinSasTransaction.MacVerificationResult.Success -> {
// mark the devices as locally trusted
result.verifiedDeviceId.forEach { deviceId ->
val actualTrustLevel = cryptoStore.getUserDevice(matchingRequest.otherUserId, deviceId)?.trustLevel
setDeviceVerificationAction.handle(
trustLevel = DeviceTrustLevel(
actualTrustLevel?.crossSigningVerified == true,
true
),
userId = matchingRequest.otherUserId,
deviceId = deviceId
)
if (matchingRequest.otherUserId == myUserId && crossSigningService.get().canCrossSign()) {
verificationTrustBackend.locallyTrustDevice(matchingRequest.otherUserId, deviceId)
if (matchingRequest.otherUserId == myUserId && verificationTrustBackend.canCrossSign()) {
// If me it's reasonable to sign and upload the device signature for the other part
try {
crossSigningService.get().trustDevice(deviceId)
verificationTrustBackend.trustOwnDevice(deviceId)
} catch (failure: Throwable) {
// network problem??
Timber.w("## Verification: Failed to sign new device $deviceId, ${failure.localizedMessage}")
@ -1245,11 +1184,11 @@ internal class VerificationActor @AssistedInject constructor(
if (result.otherMskTrusted) {
if (matchingRequest.otherUserId == myUserId) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
verificationTrustBackend.markMyMasterKeyAsTrusted()
} else {
// what should we do if this fails :/
if (crossSigningService.get().canCrossSign()) {
crossSigningService.get().trustUser(matchingRequest.otherUserId)
if (verificationTrustBackend.canCrossSign()) {
verificationTrustBackend.trustUser(matchingRequest.otherUserId)
}
}
}
@ -1272,8 +1211,8 @@ internal class VerificationActor @AssistedInject constructor(
existing.state = SasTransactionState.Done(false)
dispatchUpdate(VerificationEvent.TransactionUpdated(existing))
pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = existing
txMap[matchingRequest.otherUserId]?.remove(transactionId)
verificationRequestsStore.rememberPastSuccessfulTransaction(existing)
verificationRequestsStore.deleteTransaction(matchingRequest.otherUserId, transactionId)
matchingRequest.state = EVerificationState.WaitingForDone
dispatchUpdate(VerificationEvent.RequestUpdated(matchingRequest.toPendingVerificationRequest()))
}
@ -1287,9 +1226,7 @@ internal class VerificationActor @AssistedInject constructor(
}
private suspend fun handleActionReadyRequest(msg: VerificationIntent.ActionReadyRequest) {
val existing = pendingRequests
.flatMap { it.value }
.firstOrNull { it.requestId == msg.transactionId }
val existing = verificationRequestsStore.getExistingRequestWithRequestId(msg.transactionId)
?: return Unit.also {
Timber.tag(loggerTag.value).v("Request ${msg.transactionId} not found!")
msg.deferred.complete(null)
@ -1308,13 +1245,11 @@ internal class VerificationActor @AssistedInject constructor(
)
if (commonMethods.isEmpty()) {
Timber.tag(loggerTag.value).v("Request ${msg.transactionId} no common methods")
cancelRequest(existing, CancelCode.UnknownMethod)
// Upon receipt of Alices m.key.verification.request message, if Bobs device does not understand any of the methods,
// it should not cancel the request as one of his other devices may support the request.
// XXX How to o that??
// Instead, Bobs device should tell Bob that no supported method was found, and allow him to manually reject the request.
msg.deferred.complete(null)
msg.deferred.completeExceptionally(IllegalStateException("Cannot understand any of the methods"))
return
}
@ -1329,7 +1264,7 @@ internal class VerificationActor @AssistedInject constructor(
val readyInfo = ValidVerificationInfoReady(
msg.transactionId,
cryptoStore.getDeviceId(),
verificationTrustBackend.getMyDeviceId(),
commonMethods
)
@ -1337,7 +1272,7 @@ internal class VerificationActor @AssistedInject constructor(
inRoom = existing.roomId != null,
requestId = msg.transactionId,
methods = commonMethods,
fromDevice = cryptoStore.getDeviceId()
fromDevice = verificationTrustBackend.getMyDeviceId()
)
try {
transportLayer.sendToOther(existing, EventType.KEY_VERIFICATION_READY, message)
@ -1357,11 +1292,11 @@ internal class VerificationActor @AssistedInject constructor(
msg.deferred.complete(existing.toPendingVerificationRequest())
}
private fun createQrCodeData(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? {
private suspend fun createQrCodeData(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? {
return when {
myUserId != otherUserId ->
createQrCodeDataForDistinctUser(requestId, otherUserId)
cryptoStore.getMyCrossSigningInfo()?.isTrusted().orFalse() ->
verificationTrustBackend.getMyTrustedMasterKeyBase64() != null ->
// This is a self verification and I am the old device (Osborne2)
createQrCodeDataForVerifiedDevice(requestId, otherUserId, otherDeviceId)
else ->
@ -1410,7 +1345,7 @@ internal class VerificationActor @AssistedInject constructor(
}
private suspend fun handleActionRequestVerification(msg: VerificationIntent.ActionRequestVerification) {
val requestsForUser = pendingRequests.getOrPut(msg.otherUserId) { mutableListOf() }
val requestsForUser = verificationRequestsStore.getExistingRequestsForUser(msg.otherUserId)
// there can only be one active request per user, so cancel existing ones
requestsForUser.toList().forEach { existingRequest ->
if (!existingRequest.isFinished()) {
@ -1419,7 +1354,7 @@ internal class VerificationActor @AssistedInject constructor(
}
}
val methodValues = if (cryptoStore.getMyCrossSigningInfo()?.isTrusted().orFalse()) {
val methodValues = if (verificationTrustBackend.getMyTrustedMasterKeyBase64() != null) {
// Add reciprocate method if application declares it can scan or show QR codes
// Not sure if it ok to do that (?)
val reciprocateMethod = msg.methods
@ -1436,7 +1371,7 @@ internal class VerificationActor @AssistedInject constructor(
val validInfo = ValidVerificationInfoRequest(
transactionId = "",
fromDevice = cryptoStore.getDeviceId(),
fromDevice = verificationTrustBackend.getMyDeviceId(),
methods = methodValues,
timestamp = clock.epochMillis()
)
@ -1466,7 +1401,7 @@ internal class VerificationActor @AssistedInject constructor(
roomId = msg.roomId
requestInfo = validInfo.copy(transactionId = eventId)
}
requestsForUser.add(request)
verificationRequestsStore.addRequest(msg.otherUserId, request)
msg.deferred.complete(request.toPendingVerificationRequest())
dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest()))
} else {
@ -1475,7 +1410,7 @@ internal class VerificationActor @AssistedInject constructor(
messageType = EventType.KEY_VERIFICATION_REQUEST,
toSendToDeviceObject = KeyVerificationRequest(
transactionId = requestId,
fromDevice = cryptoStore.getDeviceId(),
fromDevice = verificationTrustBackend.getMyDeviceId(),
methods = validInfo.methods,
timestamp = validInfo.timestamp
),
@ -1493,7 +1428,7 @@ internal class VerificationActor @AssistedInject constructor(
roomId = null
requestInfo = validInfo.copy(transactionId = requestId)
}
requestsForUser.add(request)
verificationRequestsStore.addRequest(msg.otherUserId, request)
msg.deferred.complete(request.toPendingVerificationRequest())
dispatchUpdate(VerificationEvent.RequestAdded(request.toPendingVerificationRequest()))
}
@ -1505,13 +1440,13 @@ internal class VerificationActor @AssistedInject constructor(
}
private suspend fun handleReadyReceived(msg: VerificationIntent.OnReadyReceived) {
val matchingRequest = pendingRequests[msg.fromUser]?.firstOrNull { it.requestId == msg.transactionId }
val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.v("[${myUserId.take(8)}]: No matching request to ready tId:${msg.transactionId}")
// cancelRequest(msg.transactionId, msg.viaRoom, msg.fromUser, msg.readyInfo.fromDevice, CancelCode.UnknownTransaction)
}
val myDevice = cryptoStore.getDeviceId()
val myDevice = verificationTrustBackend.getMyDeviceId()
if (matchingRequest.state != EVerificationState.WaitingForReady) {
cancelRequest(matchingRequest, CancelCode.UnexpectedMessage)
@ -1552,11 +1487,11 @@ internal class VerificationActor @AssistedInject constructor(
if (msg.viaRoom == null) {
// we should cancel to others if it was requested via to_device
// via room the other session will see the ready in room an mark the transaction as inactive for them
val deviceIds = cryptoStore.getUserDevices(matchingRequest.otherUserId)?.keys
?.filter { it != msg.readyInfo.fromDevice }
val deviceIds = verificationTrustBackend.getUserDeviceList(matchingRequest.otherUserId)
.filter { it.deviceId != msg.readyInfo.fromDevice }
// if it's me we don't want to send self cancel
?.filter { it != myDevice }
.orEmpty()
.filter { it.deviceId != myDevice }
.map { it.deviceId }
try {
transportLayer.sendToDeviceEvent(
@ -1577,7 +1512,7 @@ internal class VerificationActor @AssistedInject constructor(
}
private suspend fun handleReadyByAnotherOfMySessionReceived(msg: VerificationIntent.OnReadyByAnotherOfMySessionReceived) {
val matchingRequest = pendingRequests[msg.fromUser]?.firstOrNull { it.requestId == msg.transactionId }
val matchingRequest = verificationRequestsStore.getExistingRequest(msg.fromUser, msg.transactionId)
?: return
// it's a ready from another of my devices, so we should just
@ -1602,25 +1537,21 @@ internal class VerificationActor @AssistedInject constructor(
// dispatchUpdate(VerificationEvent.RequestUpdated(updated))
// }
private suspend fun dispatchRequestAdded(tx: KotlinVerificationRequest) {
private fun dispatchRequestAdded(tx: KotlinVerificationRequest) {
Timber.v("## SAS dispatchRequestAdded txId:${tx.requestId}")
dispatchUpdate(VerificationEvent.RequestAdded(tx.toPendingVerificationRequest()))
}
// Utilities
private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? {
val myMasterKey = cryptoStore.getMyCrossSigningInfo()
?.masterKey()
?.unpaddedBase64PublicKey
private suspend fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? {
val myMasterKey = verificationTrustBackend.getMyTrustedMasterKeyBase64()
?: run {
Timber.w("## Unable to get my master key")
return null
}
val otherUserMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)
?.masterKey()
?.unpaddedBase64PublicKey
val otherUserMasterKey = verificationTrustBackend.getUserMasterKeyBase64(otherUserId)
?: run {
Timber.w("## Unable to get other user master key")
return null
@ -1635,10 +1566,8 @@ internal class VerificationActor @AssistedInject constructor(
}
// Create a QR code to display on the old device (Osborne2)
private fun createQrCodeDataForVerifiedDevice(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? {
val myMasterKey = cryptoStore.getMyCrossSigningInfo()
?.masterKey()
?.unpaddedBase64PublicKey
private suspend fun createQrCodeDataForVerifiedDevice(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? {
val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId)
?: run {
Timber.w("## Unable to get my master key")
return null
@ -1646,7 +1575,7 @@ internal class VerificationActor @AssistedInject constructor(
val otherDeviceKey = otherDeviceId
?.let {
cryptoStore.getUserDevice(otherUserId, otherDeviceId)?.fingerprint()
verificationTrustBackend.getUserDevice(otherUserId, otherDeviceId)?.fingerprint()
}
?: run {
Timber.w("## Unable to get other device data")
@ -1662,16 +1591,14 @@ internal class VerificationActor @AssistedInject constructor(
}
// Create a QR code to display on the new device (Dynabook)
private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? {
val myMasterKey = cryptoStore.getMyCrossSigningInfo()
?.masterKey()
?.unpaddedBase64PublicKey
private suspend fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? {
val myMasterKey = verificationTrustBackend.getUserMasterKeyBase64(myUserId)
?: run {
Timber.w("## Unable to get my master key")
return null
}
val myDeviceKey = cryptoStore.getUserDevice(myUserId, cryptoStore.getDeviceId())?.fingerprint()
val myDeviceKey = verificationTrustBackend.getUserDevice(myUserId, verificationTrustBackend.getMyDeviceId())?.fingerprint()
?: return null.also {
Timber.w("## Unable to get my fingerprint")
}
@ -1684,62 +1611,6 @@ 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 suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) {
request.state = EVerificationState.Cancelled
request.cancelCode = code
@ -1748,12 +1619,12 @@ internal class VerificationActor @AssistedInject constructor(
// 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)
verificationRequestsStore.deleteTransaction(request.otherUserId, request.requestId)
dispatchUpdate(VerificationEvent.TransactionUpdated(it))
}
getExistingTransaction<KotlinQRVerification>(request.otherUserId, request.requestId)?.let {
it.state = QRCodeVerificationState.Cancelled
txMap[request.otherUserId]?.remove(request.requestId)
verificationRequestsStore.deleteTransaction(request.otherUserId, request.requestId)
dispatchUpdate(VerificationEvent.TransactionUpdated(it))
}
@ -1809,30 +1680,29 @@ internal class VerificationActor @AssistedInject constructor(
private fun hashUsingAgreedHashMethod(hashMethod: String?, toHash: String): String {
if ("sha256" == hashMethod?.lowercase(Locale.ROOT)) {
return withOlmUtility {
it.sha256(toHash)
}
return olmPrimitiveProvider.sha256(toHash)
}
throw java.lang.IllegalArgumentException("Unsupported hash method $hashMethod")
}
private suspend fun addTransaction(tx: VerificationTransaction) {
val txInnerMap = txMap.getOrPut(tx.otherUserId) { mutableMapOf() }
txInnerMap[tx.transactionId] = tx
verificationRequestsStore.addTransaction(tx)
dispatchUpdate(VerificationEvent.TransactionAdded(tx))
}
private inline fun <reified T : VerificationTransaction> getExistingTransaction(otherUserId: String, transactionId: String): T? {
return txMap[otherUserId]?.get(transactionId) as? T
return verificationRequestsStore.getExistingTransaction(otherUserId, transactionId) as? T
}
private inline fun <reified T : VerificationTransaction> getExistingTransaction(transactionId: String): T? {
txMap.forEach {
val match = it.value.values
.firstOrNull { it.transactionId == transactionId }
?.takeIf { it is T }
if (match != null) return match as? T
}
return null
return verificationRequestsStore.getExistingTransaction(transactionId)
.takeIf { it is T } as? T
// txMap.forEach {
// val match = it.value.values
// .firstOrNull { it.transactionId == transactionId }
// ?.takeIf { it is T }
// if (match != null) return match as? T
// }
// return null
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.internal.crypto.tools.withOlmUtility
import org.matrix.olm.OlmSAS
import javax.inject.Inject
// Mainly for testing purpose to ease mocking
internal class VerificationCryptoPrimitiveProvider @Inject constructor() {
fun provideOlmSas(): OlmSAS {
return OlmSAS()
}
fun sha256(toHash: String): String {
return withOlmUtility {
it.sha256(toHash)
}
}
}

View File

@ -42,14 +42,6 @@ internal sealed class VerificationIntent {
// val deferred: CompletableDeferred<IVerificationRequest>,
) : VerificationIntent()
data class FailToSendRequest(
val request: PendingVerificationRequest,
) : VerificationIntent()
// data class UpdateRequest(
// val request: IVerificationRequest,
// ) : VerificationIntent()
data class ActionReadyRequest(
val transactionId: String,
val methods: List<VerificationMethod>,

View File

@ -0,0 +1,103 @@
/*
* 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.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import javax.inject.Inject
internal class VerificationRequestsStore @Inject constructor() {
// map [sender : [transaction]]
private val txMap = HashMap<String, MutableMap<String, VerificationTransaction>>()
// we need to keep track of finished transaction
// It will be used for gossiping (to send request after request is completed and 'done' by other)
private val pastTransactions = HashMap<String, MutableMap<String, VerificationTransaction>>()
/**
* Map [sender: [PendingVerificationRequest]]
* For now we keep all requests (even terminated ones) during the lifetime of the app.
*/
private val pendingRequests = HashMap<String, MutableList<KotlinVerificationRequest>>()
fun getExistingRequest(fromUser: String, requestId: String): KotlinVerificationRequest? {
return pendingRequests[fromUser]?.firstOrNull { it.requestId == requestId }
}
fun getExistingRequestsForUser(fromUser: String): List<KotlinVerificationRequest> {
return pendingRequests[fromUser].orEmpty()
}
fun getExistingRequestInRoom(requestId: String, roomId: String): KotlinVerificationRequest? {
return pendingRequests.flatMap { entry ->
entry.value.filter { it.roomId == roomId && it.requestId == requestId }
}.firstOrNull()
}
fun getExistingRequestWithRequestId(requestId: String): KotlinVerificationRequest? {
return pendingRequests
.flatMap { it.value }
.firstOrNull { it.requestId == requestId }
}
fun getExistingTransaction(fromUser: String, transactionId: String): VerificationTransaction? {
return txMap[fromUser]?.get(transactionId)
}
fun getExistingTransaction(transactionId: String): VerificationTransaction? {
txMap.forEach {
val match = it.value.values
.firstOrNull { it.transactionId == transactionId }
if (match != null) return match
}
return null
}
fun deleteTransaction(fromUser: String, transactionId: String) {
txMap[fromUser]?.remove(transactionId)
}
fun deleteRequest(request: PendingVerificationRequest) {
val requestsForUser = pendingRequests.getOrPut(request.otherUserId) { mutableListOf() }
val index = requestsForUser.indexOfFirst {
it.requestId == request.transactionId
}
if (index != -1) {
requestsForUser.removeAt(index)
}
}
// fun deleteRequest(otherUserId: String, transactionId: String) {
// txMap[otherUserId]?.remove(transactionId)
// }
fun addRequest(otherUserId: String, request: KotlinVerificationRequest) {
pendingRequests.getOrPut(otherUserId) { mutableListOf() }
.add(request)
}
fun addTransaction(transaction: VerificationTransaction) {
val txInnerMap = txMap.getOrPut(transaction.otherUserId) { mutableMapOf() }
txInnerMap[transaction.transactionId] = transaction
}
fun rememberPastSuccessfulTransaction(transaction: VerificationTransaction) {
val transactionId = transaction.transactionId
pastTransactions.getOrPut(transactionId) { mutableMapOf() }[transactionId] = transaction
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject
internal class VerificationTrustBackend @Inject constructor(
private val crossSigningService: dagger.Lazy<CrossSigningService>,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val cryptoStore: IMXCryptoStore,
@UserId private val myUserId: String,
@DeviceId private val myDeviceId: String,
) {
suspend fun getUserMasterKeyBase64(userId: String): String? {
return crossSigningService.get()?.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey
}
suspend fun getMyTrustedMasterKeyBase64(): String? {
return cryptoStore.getMyCrossSigningInfo()
?.takeIf { it.isTrusted() }
?.masterKey()
?.unpaddedBase64PublicKey
}
fun canCrossSign(): Boolean {
return crossSigningService.get().canCrossSign()
}
suspend fun trustUser(userId: String) {
crossSigningService.get().trustUser(userId)
}
suspend fun trustOwnDevice(deviceId: String) {
crossSigningService.get().trustDevice(deviceId)
}
suspend fun locallyTrustDevice(otherUserId: String, deviceId: String) {
val actualTrustLevel = getUserDevice(otherUserId, deviceId)?.trustLevel
setDeviceVerificationAction.handle(
trustLevel = DeviceTrustLevel(
actualTrustLevel?.crossSigningVerified == true,
true
),
userId = otherUserId,
deviceId = deviceId
)
}
suspend fun markMyMasterKeyAsTrusted() {
crossSigningService.get().markMyMasterKeyAsTrusted()
}
fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? {
return cryptoStore.getUserDevice(userId, deviceId)
}
fun getMyDevice(): CryptoDeviceInfo {
return getUserDevice(myUserId, myDeviceId)!!
}
fun getUserDeviceList(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
//
// suspend fun areMyCrossSigningKeysTrusted() : Boolean {
// return crossSigningService.get().isUserTrusted(myUserId)
// }
fun getMyDeviceId() = myDeviceId
}

View File

@ -16,16 +16,15 @@
package org.matrix.android.sdk.internal.crypto.verification
import io.mockk.coEvery
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,
@ -34,10 +33,10 @@ enum class StoreMode {
internal class FakeCryptoStoreForVerification(private val mode: StoreMode) {
val instance = mockk<IMXCryptoStore>()
val instance = mockk<VerificationTrustBackend>()
init {
every { instance.getDeviceId() } answers {
every { instance.getMyDeviceId() } answers {
when (mode) {
StoreMode.Alice -> aliceDevice1Id
StoreMode.Bob -> bobDeviceId
@ -47,84 +46,53 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) {
// 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.getUserDevice(bobMxId, bobDeviceId) } returns aBobDevice
every { instance.getCrossSigningInfo(aliceMxId) } answers {
every { instance.getUserDeviceList(aliceMxId) } returns listOf(aliceFirstDevice)
every { instance.getUserDeviceList(bobMxId) } returns listOf(aBobDevice)
coEvery { instance.locallyTrustDevice(any(), any()) } returns Unit
coEvery { instance.getMyTrustedMasterKeyBase64() } 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
)
aliceMSK
}
StoreMode.Bob -> {
MXCrossSigningInfo(
aliceMxId,
listOf(
aliceMSKBase.copy(trustLevel = DeviceTrustLevel(false, false)),
aliceUSKBase.copy(trustLevel = DeviceTrustLevel(false, false)),
),
wasTrustedOnce = false
)
bobMSK
}
}
}
every { instance.getCrossSigningInfo(bobMxId) } answers {
coEvery { instance.getUserMasterKeyBase64(any()) } answers {
val mxId = firstArg<String>()
when (mxId) {
aliceMxId -> aliceMSK
bobMxId -> bobMSK
else -> null
}
}
coEvery { instance.getMyDeviceId() } 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
)
}
StoreMode.Alice -> aliceDevice1Id
StoreMode.Bob -> bobDeviceId
}
}
every { instance.getMyCrossSigningInfo() } answers {
coEvery { instance.getMyDevice() } 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
)
StoreMode.Alice -> aliceFirstDevice
StoreMode.Bob -> aBobDevice
}
}
coEvery {
instance.trustOwnDevice(any())
} returns Unit
coEvery {
instance.trustUser(any())
} returns Unit
}
companion object {
@ -133,7 +101,7 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) {
val bobMxId = "bob@example.com"
val bobDeviceId = "MKRJDSLYGA"
private val aliceDevice1Id = "MGDAADVDMG"
val aliceDevice1Id = "MGDAADVDMG"
private val aliceMSK = "Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0"
private val aliceSSK = "Rw6MiEn5do57mBWlWUvL6VDZJ7vAfGrTC58UXVyA0eo"

View File

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto.verification
import dagger.Lazy
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@ -25,17 +24,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
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.api.session.room.model.message.ValidVerificationDone
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.util.time.Clock
import org.matrix.olm.OlmSAS
import java.util.UUID
internal class VerificationActorHelper {
@ -43,6 +41,8 @@ internal class VerificationActorHelper {
data class TestData(
val aliceActor: VerificationActor,
val bobActor: VerificationActor,
val aliceStore: FakeCryptoStoreForVerification,
val bobStore: FakeCryptoStoreForVerification,
)
val actorAScope = CoroutineScope(SupervisorJob())
@ -56,35 +56,29 @@ internal class VerificationActorHelper {
val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel }
val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel }
val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice)
val aliceActor = fakeActor(
actorAScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
fakeAliceStore.instance,
aliceTransportLayer,
mockk<Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
)
aliceChannel = aliceActor.channel
val fakeBobStore = FakeCryptoStoreForVerification(StoreMode.Bob)
val bobActor = fakeActor(
actorBScope,
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification(StoreMode.Alice).instance,
bobTransportLayer,
mockk<Lazy<CrossSigningService>> {
every {
get()
} returns mockk<CrossSigningService>(relaxed = true)
}
FakeCryptoStoreForVerification.bobMxId,
fakeBobStore.instance,
bobTransportLayer
)
bobChannel = bobActor.channel
return TestData(
aliceActor,
bobActor
aliceActor = aliceActor,
bobActor = bobActor,
aliceStore = fakeAliceStore,
bobStore = fakeBobStore
)
}
@ -108,6 +102,56 @@ internal class VerificationActorHelper {
)
)
}
EventType.KEY_VERIFICATION_START -> {
val startContent = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnStartReceived(
fromUser = fromUser,
viaRoom = request.roomId,
validVerificationInfoStart = startContent as ValidVerificationInfoStart,
)
)
}
EventType.KEY_VERIFICATION_ACCEPT -> {
val content = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnAcceptReceived(
fromUser = fromUser,
viaRoom = request.roomId,
validAccept = content as ValidVerificationInfoAccept,
)
)
}
EventType.KEY_VERIFICATION_KEY -> {
val content = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnKeyReceived(
fromUser = fromUser,
viaRoom = request.roomId,
validKey = content as ValidVerificationInfoKey,
)
)
}
EventType.KEY_VERIFICATION_MAC -> {
val content = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnMacReceived(
fromUser = fromUser,
viaRoom = request.roomId,
validMac = content as ValidVerificationInfoMac,
)
)
}
EventType.KEY_VERIFICATION_DONE -> {
val content = info.asValidObject()
otherChannel()?.send(
VerificationIntent.OnDoneReceived(
fromUser = fromUser,
viaRoom = request.roomId,
transactionId = (content as ValidVerificationDone).transactionId,
)
)
}
}
}
}
@ -154,9 +198,8 @@ internal class VerificationActorHelper {
private fun fakeActor(
scope: CoroutineScope,
userId: String,
cryptoStore: IMXCryptoStore,
cryptoStore: VerificationTrustBackend,
transportLayer: VerificationTransportLayer,
crossSigningService: dagger.Lazy<CrossSigningService>,
): VerificationActor {
return VerificationActor(
scope,
@ -165,17 +208,19 @@ internal class VerificationActorHelper {
every { epochMillis() } returns System.currentTimeMillis()
},
myUserId = userId,
cryptoStore = cryptoStore,
verificationTrustBackend = cryptoStore,
secretShareManager = mockk<SecretShareManager> {},
transportLayer = transportLayer,
crossSigningService = crossSigningService,
setDeviceVerificationAction = SetDeviceVerificationAction(
cryptoStore = cryptoStore,
userId = userId,
defaultKeysBackupService = mockk {
coEvery { checkAndStartKeysBackup() } coAnswers { }
verificationRequestsStore = VerificationRequestsStore(),
olmPrimitiveProvider = mockk<VerificationCryptoPrimitiveProvider> {
every { provideOlmSas() } returns mockk<OlmSAS> {
every { publicKey } returns "Tm9JRGVhRmFrZQo="
every { setTheirPublicKey(any()) } returns Unit
every { generateShortCode(any(), any()) } returns byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
every { calculateMac(any(), any()) } returns "mic mac mec"
}
every { sha256(any()) } returns "fake_hash"
}
)
)
}
}

View File

@ -17,7 +17,11 @@
package org.matrix.android.sdk.internal.crypto.verification.org.matrix.android.sdk.internal.crypto.verification
import android.util.Base64
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockkConstructor
import io.mockk.mockkStatic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@ -26,6 +30,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
@ -34,23 +41,29 @@ import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertNotEquals
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldNotBe
import org.amshove.kluent.shouldNotBeEqualTo
import org.json.JSONObject
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.MatrixTest
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
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.getRequest
import org.matrix.android.sdk.internal.crypto.verification.FakeCryptoStoreForVerification
import org.matrix.android.sdk.internal.crypto.verification.VerificationActor
import org.matrix.android.sdk.internal.crypto.verification.VerificationActorHelper
import org.matrix.android.sdk.internal.crypto.verification.VerificationIntent
@OptIn(ExperimentalCoroutinesApi::class)
class VerificationActorTest {
class VerificationActorTest : MatrixTest {
val transportScope = CoroutineScope(SupervisorJob())
// val actorAScope = CoroutineScope(SupervisorJob())
// val actorBScope = CoroutineScope(SupervisorJob())
@Before
fun setUp() {
@ -69,6 +82,20 @@ class VerificationActorTest {
val array = firstArg<String>()
java.util.Base64.getDecoder().decode(array)
}
// to mock canonical json
mockkConstructor(JSONObject::class)
every { anyConstructed<JSONObject>().keys() } returns emptyList<String>().listIterator()
// mockkConstructor(KotlinSasTransaction::class)
// every { anyConstructed<KotlinSasTransaction>().getSAS() } returns mockk<OlmSAS>() {
// every { publicKey } returns "Tm9JRGVhRmFrZQo="
// }
}
@After
fun tearDown() {
clearAllMocks()
}
@Test
@ -170,6 +197,39 @@ class VerificationActorTest {
assertEquals("Bob should not show QR as alice can scan", false, bobReady.weShouldDisplayQRCode)
}
@Test
fun `If Bobs device does not understand any of the methods, it should not cancel the request`() = runTest {
val testData = VerificationActorHelper().setUpActors()
val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
val outgoingRequest = aliceActor.requestVerification(
listOf(VerificationMethod.SAS)
)
// wait for bob to get it
waitForBobToSeeIncomingRequest(bobActor, outgoingRequest)
println("let bob ready it")
try {
bobActor.readyVerification(
outgoingRequest.transactionId,
listOf(VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
)
// Upon receipt of Alices m.key.verification.request message, if Bobs device does not understand any of the methods,
// it should not cancel the request as one of his other devices may support the request
fail("Ready should fail as no common methods")
} catch (failure: Throwable) {
// should throw
}
val bodSide = awaitDeferrable<PendingVerificationRequest?> {
bobActor.send(VerificationIntent.GetExistingRequest(outgoingRequest.transactionId, FakeCryptoStoreForVerification.aliceMxId, it))
}!!
bodSide.state shouldNotBeEqualTo EVerificationState.Cancelled
}
@Test
fun `Test bob can show but not scan QR`() = runTest {
val testData = VerificationActorHelper().setUpActors()
@ -215,6 +275,199 @@ class VerificationActorTest {
assertEquals("Bob should show QR as alice can scan", true, bobReady.weShouldDisplayQRCode)
}
@Test
fun `Test verify to users that trust their cross signing keys`() = runTest {
val testData = VerificationActorHelper().setUpActors()
val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
coEvery { testData.bobStore.instance.canCrossSign() } returns true
coEvery { testData.aliceStore.instance.canCrossSign() } returns true
fullSasVerification(bobActor, aliceActor)
coVerify {
testData.bobStore.instance.locallyTrustDevice(
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification.aliceDevice1Id,
)
}
coVerify {
testData.bobStore.instance.trustUser(
FakeCryptoStoreForVerification.aliceMxId
)
}
coVerify {
testData.aliceStore.instance.locallyTrustDevice(
FakeCryptoStoreForVerification.bobMxId,
FakeCryptoStoreForVerification.bobDeviceId,
)
}
coVerify {
testData.aliceStore.instance.trustUser(
FakeCryptoStoreForVerification.bobMxId
)
}
}
@Test
fun `Test user verification when alice do not trust her keys`() = runTest {
val testData = VerificationActorHelper().setUpActors()
val aliceActor = testData.aliceActor
val bobActor = testData.bobActor
coEvery { testData.bobStore.instance.canCrossSign() } returns true
coEvery { testData.aliceStore.instance.canCrossSign() } returns false
coEvery { testData.aliceStore.instance.getMyTrustedMasterKeyBase64() } returns null
fullSasVerification(bobActor, aliceActor)
coVerify {
testData.bobStore.instance.locallyTrustDevice(
FakeCryptoStoreForVerification.aliceMxId,
FakeCryptoStoreForVerification.aliceDevice1Id,
)
}
coVerify(exactly = 0) {
testData.bobStore.instance.trustUser(
FakeCryptoStoreForVerification.aliceMxId
)
}
coVerify {
testData.aliceStore.instance.locallyTrustDevice(
FakeCryptoStoreForVerification.bobMxId,
FakeCryptoStoreForVerification.bobDeviceId,
)
}
coVerify(exactly = 0) {
testData.aliceStore.instance.trustUser(
FakeCryptoStoreForVerification.bobMxId
)
}
}
private suspend fun fullSasVerification(bobActor: VerificationActor, aliceActor: VerificationActor) {
transportScope.launch {
bobActor.eventFlow
.collect {
println("Bob flow 1 event $it")
if (it is VerificationEvent.RequestAdded) {
// auto accept
bobActor.readyVerification(
it.transactionId,
listOf(VerificationMethod.SAS)
)
// then start
bobActor.send(
VerificationIntent.ActionStartSasVerification(
FakeCryptoStoreForVerification.aliceMxId,
it.transactionId,
CompletableDeferred()
)
)
}
return@collect cancel()
}
}
val aliceCode = CompletableDeferred<SasVerificationTransaction>()
val bobCode = CompletableDeferred<SasVerificationTransaction>()
aliceActor.eventFlow.onEach {
println("Alice flow event $it")
if (it is VerificationEvent.TransactionUpdated) {
val sasVerificationTransaction = it.transaction as SasVerificationTransaction
if (sasVerificationTransaction.state() is SasTransactionState.SasShortCodeReady) {
aliceCode.complete(sasVerificationTransaction)
}
}
}.launchIn(transportScope)
bobActor.eventFlow.onEach {
println("Bob flow event $it")
if (it is VerificationEvent.TransactionUpdated) {
val sasVerificationTransaction = it.transaction as SasVerificationTransaction
if (sasVerificationTransaction.state() is SasTransactionState.SasShortCodeReady) {
bobCode.complete(sasVerificationTransaction)
}
}
}.launchIn(transportScope)
println("Alice sends a request")
val outgoingRequest = aliceActor.requestVerification(
listOf(VerificationMethod.SAS)
)
// asserting the code won't help much here as all is mocked
// we are checking state progression
// Both transaction should be in sas ready
val aliceCodeReadyTx = aliceCode.await()
bobCode.await()
// If alice accept the code, bob should pass to state mac received but code not comfirmed
aliceCodeReadyTx.userHasVerifiedShortCode()
retryUntil {
val tx = bobActor.getTransactionBobPov(outgoingRequest.transactionId)
val sasTx = tx as? SasVerificationTransaction
val state = sasTx?.state()
(state is SasTransactionState.SasMacReceived && !state.codeConfirmed)
}
val bobTransaction = bobActor.getTransactionBobPov(outgoingRequest.transactionId) as SasVerificationTransaction
val bobDone = CompletableDeferred(Unit)
val aliceDone = CompletableDeferred(Unit)
transportScope.launch {
bobActor.eventFlow
.collect {
println("Bob flow 1 event $it")
it.getRequest()?.let {
if (it.transactionId == outgoingRequest.transactionId && it.state == EVerificationState.Done) {
bobDone.complete(Unit)
return@collect cancel()
}
}
}
}
transportScope.launch {
aliceActor.eventFlow
.collect {
println("Bob flow 1 event $it")
it.getRequest()?.let {
if (it.transactionId == outgoingRequest.transactionId && it.state == EVerificationState.Done) {
bobDone.complete(Unit)
return@collect cancel()
}
}
}
}
// mark as verified from bob side
bobTransaction.userHasVerifiedShortCode()
aliceDone.await()
bobDone.await()
}
internal suspend fun VerificationActor.getTransactionBobPov(transactionId: String): VerificationTransaction? {
return awaitDeferrable<VerificationTransaction?> {
channel.send(
VerificationIntent.GetExistingTransaction(
transactionId = transactionId,
fromUser = FakeCryptoStoreForVerification.aliceMxId,
it
)
)
}
}
private suspend fun VerificationActor.requestVerification(methods: List<VerificationMethod>): PendingVerificationRequest {
return awaitDeferrable<PendingVerificationRequest> {
send(
@ -273,74 +526,4 @@ class VerificationActorTest {
)
}!!
}
// @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")}")
// }
// @Test
// fun `Basic channel test`() {
// // val sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 2, BufferOverflow.DROP_OLDEST)
// val sharedFlow = MutableSharedFlow<Int>(replay = 0)//, extraBufferCapacity = 0, BufferOverflow.DROP_OLDEST)
//
// val scope = CoroutineScope(SupervisorJob())
// val deferred = CompletableDeferred<Unit>()
// val listener = scope.launch {
// sharedFlow.onEach {
// println("L1 : Just collected $it")
// delay(1000)
// println("L1 : Just processed $it")
// if (it == 2) {
// deferred.complete(Unit)
// }
// }.launchIn(scope)
// }
//
// // scope.launch {
// // delay(700)
// println("Pre Emit 1")
// sharedFlow.tryEmit(1)
// println("Emited 1")
// sharedFlow.tryEmit(2)
// println("Emited 2")
// // }
//
// // runBlocking {
// // deferred.await()
// // }
//
// sharedFlow.onEach {
// println("L2: Just collected $it")
// delay(1000)
// println("L2: Just processed $it")
// }.launchIn(scope)
//
//
// runBlocking {
// deferred.await()
// }
//
// val now = System.currentTimeMillis()
// println("Just give some time for execution")
// val job = scope.launch { delay(10_000) }
// runBlocking {
// job.join()
// }
// println("enough ${System.currentTimeMillis() - now}")
// }
}