diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt index f9b9b6a1bc..2e2b846a32 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/KotlinSasTransaction.kt @@ -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 } } diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt index 8ac7944a95..b08bd58c5c 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt @@ -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 -// ) - 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, 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>() - - // 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>() - - /** - * Map [sender: [PendingVerificationRequest]] - * For now we keep all requests (even terminated ones) during the lifetime of the app. - */ - private val pendingRequests = HashMap>() - // 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(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val eventFlow = MutableSharedFlow(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 { - msg.deferred.complete(it) - } + 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) } // Bob’s device ensures that it has a copy of Alice’s 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( // Alice’s 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 Alice’s 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 Alice’s m.key.verification.request message, if Bob’s 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, Bob’s 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) { -// // TODO currently to device verification messages are sent unencrypted -// // as per spec not recommended -// // > verification messages may be sent unencrypted, though this is not encouraged. -// -// val contentMap = MXUsersDevicesMap() -// -// targetDevices.forEach { -// contentMap.setObject(otherUserId, it, toSendToDeviceObject) -// } -// -// sendToDeviceTask -// .execute(SendToDeviceTask.Params(messageType, contentMap)) -// } -// -// suspend fun sendToOther( -// request: KotlinVerificationRequest, -// type: String, -// verificationInfo: VerificationInfo<*>, -// ) { -// val roomId = request.roomId -// if (roomId != null) { -// val event = createEventAndLocalEcho( -// type = type, -// roomId = roomId, -// content = verificationInfo.toEventContent()!! -// ) -// sendEventInRoom(event) -// } else { -// sendToDeviceEvent( -// type, -// verificationInfo.toSendToDeviceObject()!!, -// request.otherUserId, -// request.otherDeviceId()?.let { listOf(it) }.orEmpty() -// ) -// } -// } - private suspend fun cancelRequest(request: KotlinVerificationRequest, code: CancelCode) { request.state = EVerificationState.Cancelled request.cancelCode = code @@ -1748,12 +1619,12 @@ internal class VerificationActor @AssistedInject constructor( // should also update SAS/QR transaction getExistingTransaction(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(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 getExistingTransaction(otherUserId: String, transactionId: String): T? { - return txMap[otherUserId]?.get(transactionId) as? T + return verificationRequestsStore.getExistingTransaction(otherUserId, transactionId) as? T } private inline fun 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 } } diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt new file mode 100644 index 0000000000..b0bcbb2e04 --- /dev/null +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationCryptoPrimitiveProvider.kt @@ -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) + } + } +} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt index 6fb0a09b48..2d988d4a6b 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationIntent.kt @@ -42,14 +42,6 @@ internal sealed class VerificationIntent { // val deferred: CompletableDeferred, ) : VerificationIntent() - data class FailToSendRequest( - val request: PendingVerificationRequest, - ) : VerificationIntent() - -// data class UpdateRequest( -// val request: IVerificationRequest, -// ) : VerificationIntent() - data class ActionReadyRequest( val transactionId: String, val methods: List, diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt new file mode 100644 index 0000000000..5a4748c5b4 --- /dev/null +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequestsStore.kt @@ -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>() + + // 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>() + + /** + * Map [sender: [PendingVerificationRequest]] + * For now we keep all requests (even terminated ones) during the lifetime of the app. + */ + private val pendingRequests = HashMap>() + + fun getExistingRequest(fromUser: String, requestId: String): KotlinVerificationRequest? { + return pendingRequests[fromUser]?.firstOrNull { it.requestId == requestId } + } + + fun getExistingRequestsForUser(fromUser: String): List { + 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 + } +} diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt new file mode 100644 index 0000000000..7aa879bb91 --- /dev/null +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTrustBackend.kt @@ -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, + 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 { + return cryptoStore.getUserDeviceList(userId).orEmpty() + } +// +// suspend fun areMyCrossSigningKeysTrusted() : Boolean { +// return crossSigningService.get().isUserTrusted(myUserId) +// } + + fun getMyDeviceId() = myDeviceId +} diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt index 5f731d7b53..8351d021b8 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt @@ -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() + val instance = mockk() 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() + 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" diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt index d95a2733a1..ceebfc5f6e 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt @@ -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> { - every { - get() - } returns mockk(relaxed = true) - } ) aliceChannel = aliceActor.channel + val fakeBobStore = FakeCryptoStoreForVerification(StoreMode.Bob) val bobActor = fakeActor( actorBScope, - FakeCryptoStoreForVerification.aliceMxId, - FakeCryptoStoreForVerification(StoreMode.Alice).instance, - bobTransportLayer, - mockk> { - every { - get() - } returns mockk(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, ): VerificationActor { return VerificationActor( scope, @@ -165,17 +208,19 @@ internal class VerificationActorHelper { every { epochMillis() } returns System.currentTimeMillis() }, myUserId = userId, - cryptoStore = cryptoStore, + verificationTrustBackend = cryptoStore, secretShareManager = mockk {}, transportLayer = transportLayer, - crossSigningService = crossSigningService, - setDeviceVerificationAction = SetDeviceVerificationAction( - cryptoStore = cryptoStore, - userId = userId, - defaultKeysBackupService = mockk { - coEvery { checkAndStartKeysBackup() } coAnswers { } - } - ) + verificationRequestsStore = VerificationRequestsStore(), + olmPrimitiveProvider = mockk { + every { provideOlmSas() } returns mockk { + 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" + } ) } } diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt index 904f8a6219..496ec60688 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorTest.kt @@ -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() java.util.Base64.getDecoder().decode(array) } + + // to mock canonical json + mockkConstructor(JSONObject::class) + every { anyConstructed().keys() } returns emptyList().listIterator() + +// mockkConstructor(KotlinSasTransaction::class) +// every { anyConstructed().getSAS() } returns mockk() { +// 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 Alice’s m.key.verification.request message, if Bob’s 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 { + 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() + val bobCode = CompletableDeferred() + + 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 { + channel.send( + VerificationIntent.GetExistingTransaction( + transactionId = transactionId, + fromUser = FakeCryptoStoreForVerification.aliceMxId, + it + ) + ) + } + } + private suspend fun VerificationActor.requestVerification(methods: List): PendingVerificationRequest { return awaitDeferrable { send( @@ -273,74 +526,4 @@ class VerificationActorTest { ) }!! } - -// @Test -// fun `Every testing`() { -// val mockStore = mockk() -// every { mockStore.getDeviceId() } returns "A" -// println("every ${mockStore.getDeviceId()}") -// every { mockStore.getDeviceId() } returns "B" -// println("every ${mockStore.getDeviceId()}") -// -// every { mockStore.getDeviceId() } returns "A" -// every { mockStore.getDeviceId() } returns "B" -// println("every ${mockStore.getDeviceId()}") -// -// every { mockStore.getCrossSigningInfo(any()) } returns null -// every { mockStore.getCrossSigningInfo("alice") } returns MXCrossSigningInfo("alice", emptyList(), false) -// -// println("XS ${mockStore.getCrossSigningInfo("alice")}") -// println("XS ${mockStore.getCrossSigningInfo("bob")}") -// } - -// @Test -// fun `Basic channel test`() { -// // val sharedFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 2, BufferOverflow.DROP_OLDEST) -// val sharedFlow = MutableSharedFlow(replay = 0)//, extraBufferCapacity = 0, BufferOverflow.DROP_OLDEST) -// -// val scope = CoroutineScope(SupervisorJob()) -// val deferred = CompletableDeferred() -// 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}") -// } }