Update verification signaling & handing

fix encryption hindering verification
This commit is contained in:
valere 2023-01-12 12:06:57 +01:00
parent 02dc13e38d
commit 3f29c55479
15 changed files with 405 additions and 325 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3f303e8830bb4bd7005b2a166118d7771ed07259822ebb6f888abb0ed459f0cc oid sha256:a5b58eecbbaa8354901a57c7df727eb2ad6e401dd286ecf049d7a438788ac6ef
size 50458247 size 16077430

View File

@ -17,6 +17,14 @@
package org.matrix.android.sdk.common package org.matrix.android.sdk.common
import android.util.Log import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.amshove.kluent.fail import org.amshove.kluent.fail
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -38,8 +46,13 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
@ -359,99 +372,153 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
} }
suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
val scope = CoroutineScope(SupervisorJob())
assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val aliceVerificationService = alice.cryptoService().verificationService() val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService()
val localId = UUID.randomUUID().toString() val bobSeesVerification = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request != null) {
bobSeesVerification.complete(request)
return@collect cancel()
}
}
}
val aliceReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
aliceReady.complete(request)
return@collect cancel()
}
}
}
val bobReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
bobReady.complete(request)
return@collect cancel()
}
}
}
val requestID = aliceVerificationService.requestKeyVerificationInDMs( val requestID = aliceVerificationService.requestKeyVerificationInDMs(
localId = localId,
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
otherUserId = bob.myUserId, otherUserId = bob.myUserId,
roomId = roomId roomId = roomId
).transactionId ).transactionId
testHelper.retryWithBackoff( bobSeesVerification.await()
onFail = {
fail("Bob should see an incoming request from alice")
}
) {
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.otherDeviceId == alice.sessionParams.deviceId
} != null
}
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
it.otherDeviceId == alice.sessionParams.deviceId
}
Timber.v("#TEST Incoming request is $incomingRequest")
Timber.v("#TEST let bob ready the verification with SAS method")
bobVerificationService.readyPendingVerification( bobVerificationService.readyPendingVerification(
listOf(VerificationMethod.SAS), listOf(VerificationMethod.SAS),
alice.myUserId, alice.myUserId,
incomingRequest.transactionId requestID
) )
aliceReady.await()
bobReady.await()
// wait for it to be readied val bobCode = CompletableDeferred<SasVerificationTransaction>()
testHelper.retryWithBackoff(
onFail = { scope.launch(Dispatchers.IO) {
fail("Alice should see the verification in ready state") bobVerificationService.requestEventFlow()
} .cancellable()
) { .collect {
val outgoingRequest = aliceVerificationService.getExistingVerificationRequest(bob.myUserId, requestID) val transaction = it.getTransaction()
outgoingRequest?.state == EVerificationState.Ready Timber.d("#TEST flow ${bob.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val tx = transaction as? SasVerificationTransaction
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
bobCode.complete(tx)
return@collect cancel()
}
if (it.getRequest()?.state == EVerificationState.Cancelled) {
bobCode.completeExceptionally(AssertionError("Request as been cancelled"))
return@collect cancel()
}
}
}
val aliceCode = CompletableDeferred<SasVerificationTransaction>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val transaction = it.getTransaction()
Timber.d("#TEST flow ${alice.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val tx = transaction as? SasVerificationTransaction
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
aliceCode.complete(tx)
return@collect cancel()
}
if (it.getRequest()?.state == EVerificationState.Cancelled) {
aliceCode.completeExceptionally(AssertionError("Request as been cancelled"))
return@collect cancel()
}
}
} }
Timber.v("#TEST let alice start the verification") Timber.v("#TEST let alice start the verification")
aliceVerificationService.startKeyVerification( val id = aliceVerificationService.startKeyVerification(
VerificationMethod.SAS, VerificationMethod.SAS,
bob.myUserId, bob.myUserId,
requestID, requestID,
) )
Timber.v("#TEST alice started: $id")
// we should reach SHOW SAS on both val bobTx = bobCode.await()
var alicePovTx: SasVerificationTransaction? = null val aliceTx = aliceCode.await()
var bobPovTx: SasVerificationTransaction? = null assertEquals("SAS code do not match", aliceTx.getDecimalCodeRepresentation()!!, bobTx.getDecimalCodeRepresentation())
testHelper.retryWithBackoff( val aliceDone = CompletableDeferred<Unit>()
onFail = { scope.launch(Dispatchers.IO) {
fail("Alice should should see a verification code") aliceVerificationService.requestEventFlow()
} .cancellable()
) { .collect {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) val request = it.getRequest()
as? SasVerificationTransaction if (request?.state == EVerificationState.Done) {
Log.v("TEST", "== alicePovTx id:${requestID} is ${alicePovTx?.state()}") aliceDone.complete(Unit)
alicePovTx?.getDecimalCodeRepresentation() != null return@collect cancel()
}
}
} }
// wait for alice to get the ready val bobDone = CompletableDeferred<Unit>()
testHelper.retryWithBackoff( scope.launch(Dispatchers.IO) {
onFail = { bobVerificationService.requestEventFlow()
fail("Bob should should see a verification code") .cancellable()
} .collect {
) { val request = it.getRequest()
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) if (request?.state == EVerificationState.Done) {
as? SasVerificationTransaction bobDone.complete(Unit)
Log.v("TEST", "== bobPovTx is ${bobPovTx?.state()}") return@collect cancel()
// bobPovTx?.state == VerificationTxState.ShortCodeReady }
bobPovTx?.getDecimalCodeRepresentation() != null }
} }
bobTx.userHasVerifiedShortCode()
aliceTx.userHasVerifiedShortCode()
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) bobDone.await()
aliceDone.await()
bobPovTx!!.userHasVerifiedShortCode() alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
alicePovTx!!.userHasVerifiedShortCode() bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
testHelper.retryWithBackoff { scope.cancel()
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
testHelper.retryWithBackoff {
bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
}
} }
suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log import android.util.Log
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.amshove.kluent.fail import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
@ -44,6 +45,8 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@ -91,7 +94,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice is sending the message") Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my message" val text = "This is my message"
val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
Assert.assertTrue("Message should be sent", sentEventId != null) Assert.assertTrue("Message should be sent", sentEventId != null)
// All should be able to decrypt // All should be able to decrypt
@ -140,7 +143,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends a new message") Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message" val secondMessage = "2 This is my message"
val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
// new members should be able to decrypt it // new members should be able to decrypt it
newAccount.forEach { otherSession -> newAccount.forEach { otherSession ->
@ -203,7 +206,7 @@ class E2eeSanityTests : InstrumentedTest {
val sentEventIds = mutableListOf<String>() val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text -> messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
sentEventIds.add(it) sentEventIds.add(it)
} }
@ -318,7 +321,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text -> messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
sentEventIds.add(it) sentEventIds.add(it)
} }
@ -342,45 +345,24 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Create a new session for Bob") Log.v("#E2E TEST", "Create a new session for Bob")
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// ensure first session is aware of the new one
bobSession.cryptoService().downloadKeysIfNeeded(listOf(bobSession.myUserId), true)
// check that new bob can't currently decrypt // check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt") Log.v("#E2E TEST", "check that new bob can't currently decrypt")
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Try to request // Try to request
//
Log.v("#E2E TEST", "Let bob re-request") // Log.v("#E2E TEST", "Let bob re-request")
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
}
// Ensure that new bob still can't decrypt (keys must have been withheld)
// sentEventIds.forEach { sentEventId -> // sentEventIds.forEach { sentEventId ->
// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! // val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
// .getTimelineEvent(sentEventId)!! // newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
// .root.content.toModel<EncryptedEventContent>()!!.sessionId
// testHelper.retryPeriodically {
// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .first {
// it.sessionId == megolmSessionId &&
// it.roomId == e2eRoomID
// }
// .results.also {
// Log.w("##TEST", "result list is $it")
// }
// .firstOrNull { it.userId == aliceSession.myUserId }
// ?.result
// aliceReply != null &&
// aliceReply is RequestResult.Failure &&
// WithHeldCode.UNAUTHORISED == aliceReply.code
// }
// } // }
//
// */ // Log.v("#E2E TEST", "Should not be able to decrypt as not verified")
// cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
Log.v("#E2E TEST", "Should not be able to decrypt as not verified")
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Now mark new bob session as verified // Now mark new bob session as verified
@ -422,7 +404,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
firstMessage.let { text -> firstMessage.let { text ->
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
testHelper.retryWithBackoff { testHelper.retryWithBackoff {
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
@ -448,7 +430,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
secondMessage.let { text -> secondMessage.let { text ->
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
testHelper.retryWithBackoff { testHelper.retryWithBackoff {
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
@ -511,26 +493,30 @@ class E2eeSanityTests : InstrumentedTest {
} }
} }
private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
var sentEventId: String? = null
aliceRoomPOV.sendService().sendTextMessage(text) aliceRoomPOV.sendService().sendTextMessage(text)
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start() timeline.start()
testHelper.retryWithBackoff {
val decryptedMsg = timeline.getSnapshot() val messageSent = CompletableDeferred<String>()
.filter { it.root.getClearType() == EventType.MESSAGE } timeline.addListener(object : Timeline.Listener {
.also { list -> override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } val decryptedMsg = timeline.getSnapshot()
Log.v("#E2E TEST", "Timeline snapshot is $message") .filter { it.root.getClearType() == EventType.MESSAGE }
} .also { list ->
.filter { it.root.sendState == SendState.SYNCED } val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true } Log.v("#E2E TEST", "Timeline snapshot is $message")
sentEventId = decryptedMsg?.eventId }
decryptedMsg != null .filter { it.root.sendState == SendState.SYNCED }
} .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
timeline.dispose() if (decryptedMsg != null) {
return sentEventId timeline.dispose()
messageSent.complete(decryptedMsg.eventId)
}
}
})
return messageSent.await()
} }
/** /**
@ -646,7 +632,7 @@ class E2eeSanityTests : InstrumentedTest {
val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!! val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!!
Timber.v("#TEST: Send a first message that should be withheld") Timber.v("#TEST: Send a first message that should be withheld")
val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!! val sentEvent = sendMessageInRoom(roomFromAlicePOV, "Hello")!!
// wait for it to be synced back the other side // wait for it to be synced back the other side
Timber.v("#TEST: Wait for message to be synced back") Timber.v("#TEST: Wait for message to be synced back")
@ -669,7 +655,7 @@ class E2eeSanityTests : InstrumentedTest {
Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt") Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt")
val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!! val secondEvent = sendMessageInRoom(roomFromAlicePOV, "World")!!
Timber.v("#TEST: Wait for message to be synced back") Timber.v("#TEST: Wait for message to be synced back")
testHelper.retryWithBackoff { testHelper.retryWithBackoff {
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.crypto.verification package org.matrix.android.sdk.api.session.crypto.verification
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification
interface VerificationTransaction { interface VerificationTransaction {
val method: VerificationMethod val method: VerificationMethod
@ -38,3 +40,11 @@ interface VerificationTransaction {
fun isSuccessful(): Boolean fun isSuccessful(): Boolean
} }
internal fun VerificationTransaction.dbgState(): String? {
return when (this) {
is SasVerificationTransaction -> "${this.state()}"
is QrCodeVerification -> "${this.state()}"
else -> "??"
}
}

View File

@ -1,46 +0,0 @@
// /*
// * Copyright (c) 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
//
// import org.matrix.android.sdk.api.logger.LoggerTag
// import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
// 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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
// import org.matrix.android.sdk.internal.util.time.Clock
// import timber.log.Timber
// import javax.inject.Inject
//
// private val loggerTag = LoggerTag("EncryptEventContentUseCase", LoggerTag.CRYPTO)
//
// internal class EncryptEventContentUseCase @Inject constructor(
// private val olmDevice: MXOlmDevice,
// private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
// private val clock: Clock) {
//
// suspend operator fun invoke(
// eventContent: Content,
// eventType: String,
// roomId: String): MXEncryptEventContentResult {
// val t0 = clock.epochMillis()
// ensureOlmSessionsForDevicesAction.handle()
// prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false)
// val content = olmMachine.encrypt(roomId, eventType, eventContent)
// Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
// return MXEncryptEventContentResult(content, EventType.ENCRYPTED)
// }
// }

View File

@ -22,45 +22,50 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class VerificationListenersHolder @Inject constructor( internal class VerificationListenersHolder @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers coroutineDispatchers: MatrixCoroutineDispatchers,
@UserId myUserId: String,
) { ) {
val myUserId = myUserId.take(5)
val scope = CoroutineScope(SupervisorJob() + coroutineDispatchers.dmVerif) val scope = CoroutineScope(SupervisorJob() + coroutineDispatchers.dmVerif)
val eventFlow = MutableSharedFlow<VerificationEvent>(extraBufferCapacity = 20, onBufferOverflow = BufferOverflow.SUSPEND) val eventFlow = MutableSharedFlow<VerificationEvent>(extraBufferCapacity = 20, onBufferOverflow = BufferOverflow.SUSPEND)
fun dispatchTxAdded(tx: VerificationTransaction) { fun dispatchTxAdded(tx: VerificationTransaction) {
scope.launch { scope.launch {
Timber.v("## SAS [$myUserId] dispatchTxAdded txId:${tx.transactionId} | ${tx.dbgState()}")
eventFlow.emit(VerificationEvent.TransactionAdded(tx)) eventFlow.emit(VerificationEvent.TransactionAdded(tx))
} }
} }
fun dispatchTxUpdated(tx: VerificationTransaction) { fun dispatchTxUpdated(tx: VerificationTransaction) {
scope.launch { scope.launch {
Timber.v("## SAS dispatchTxUpdated txId:${tx.transactionId} $tx") Timber.v("## SAS [$myUserId] dispatchTxUpdated txId:${tx.transactionId} | ${tx.dbgState()}")
eventFlow.emit(VerificationEvent.TransactionUpdated(tx)) eventFlow.emit(VerificationEvent.TransactionUpdated(tx))
} }
} }
fun dispatchRequestAdded(verificationRequest: PendingVerificationRequest) { fun dispatchRequestAdded(verificationRequest: VerificationRequest) {
scope.launch { scope.launch {
Timber.v("## SAS dispatchRequestAdded txId:${verificationRequest.transactionId} $verificationRequest") Timber.v("## SAS [$myUserId] dispatchRequestAdded txId:${verificationRequest.flowId()} state:${verificationRequest.innerState()}")
eventFlow.emit(VerificationEvent.RequestAdded(verificationRequest)) eventFlow.emit(VerificationEvent.RequestAdded(verificationRequest.toPendingVerificationRequest()))
} }
} }
fun dispatchRequestUpdated(verificationRequest: PendingVerificationRequest) { fun dispatchRequestUpdated(verificationRequest: VerificationRequest) {
Timber.v("## SAS dispatchRequestUpdated txId:${verificationRequest.transactionId} $verificationRequest")
scope.launch { scope.launch {
eventFlow.emit(VerificationEvent.RequestUpdated(verificationRequest)) Timber.v("## SAS [$myUserId] dispatchRequestUpdated txId:${verificationRequest.flowId()} state:${verificationRequest.innerState()}")
eventFlow.emit(VerificationEvent.RequestUpdated(verificationRequest.toPendingVerificationRequest()))
} }
} }
} }

View File

@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.events.model.Content 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.EventType
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -36,9 +38,24 @@ internal class EncryptEventContentUseCase @Inject constructor(
eventType: String, eventType: String,
roomId: String): MXEncryptEventContentResult { roomId: String): MXEncryptEventContentResult {
val t0 = clock.epochMillis() val t0 = clock.epochMillis()
prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false)
/**
* When using in-room messages and the room has encryption enabled,
* clients should ensure that encryption does not hinder the verification.
* For example, if the verification messages are encrypted, clients must ensure that all the recipients
* unverified devices receive the keys necessary to decrypt the messages,
* even if they would normally not be given the keys to decrypt messages in the room.
*/
val shouldSendToUnverified = isVerificationEvent(eventType, eventContent)
prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false, forceDistributeToUnverified = shouldSendToUnverified)
val content = olmMachine.encrypt(roomId, eventType, eventContent) val content = olmMachine.encrypt(roomId, eventType, eventContent)
Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
return MXEncryptEventContentResult(content, EventType.ENCRYPTED) return MXEncryptEventContentResult(content, EventType.ENCRYPTED)
} }
private fun isVerificationEvent(eventType: String, eventContent: Content) =
EventType.isVerificationEvent(eventType) ||
(eventType == EventType.MESSAGE &&
eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST)
} }

View File

@ -298,15 +298,19 @@ internal class OlmMachine @Inject constructor(
return ToDeviceSyncResponse(events = response) return ToDeviceSyncResponse(events = response)
} }
//
// suspend fun receiveUnencryptedVerificationEvent(roomId: String, event: Event) = withContext(coroutineDispatchers.io) {
// val adapter = moshi
// .adapter(Event::class.java)
// val serializedEvent = adapter.toJson(event)
// inner.receiveUnencryptedVerificationEvent(serializedEvent, roomId)
// }
suspend fun receiveUnencryptedVerificationEvent(roomId: String, event: Event) = withContext(coroutineDispatchers.io) { suspend fun receiveVerificationEvent(roomId: String, event: Event) = withContext(coroutineDispatchers.io) {
val adapter = moshi val adapter = moshi
.newBuilder()
.add(CheckNumberType.JSON_ADAPTER_FACTORY)
.build()
.adapter(Event::class.java) .adapter(Event::class.java)
val serializedEvent = adapter.toJson(event) val serializedEvent = adapter.toJson(event)
inner.receiveUnencryptedVerificationEvent(serializedEvent, roomId) inner.receiveVerificationEvent(serializedEvent, roomId)
} }
/** /**

View File

@ -57,7 +57,7 @@ internal class PrepareToEncryptUseCase @Inject constructor(
private val keyClaimLock: Mutex = Mutex() private val keyClaimLock: Mutex = Mutex()
private val roomKeyShareLocks: ConcurrentHashMap<String, Mutex> = ConcurrentHashMap() private val roomKeyShareLocks: ConcurrentHashMap<String, Mutex> = ConcurrentHashMap()
suspend operator fun invoke(roomId: String, ensureAllMembersAreLoaded: Boolean) { suspend operator fun invoke(roomId: String, ensureAllMembersAreLoaded: Boolean, forceDistributeToUnverified: Boolean = false) {
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
// Ensure to load all room members // Ensure to load all room members
@ -76,7 +76,7 @@ internal class PrepareToEncryptUseCase @Inject constructor(
Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason")
throw IllegalArgumentException("Missing algorithm") throw IllegalArgumentException("Missing algorithm")
} }
preshareRoomKey(roomId, userIds) preshareRoomKey(roomId, userIds, forceDistributeToUnverified)
} }
} }
@ -84,7 +84,7 @@ internal class PrepareToEncryptUseCase @Inject constructor(
return cryptoStore.getRoomAlgorithm(roomId) return cryptoStore.getRoomAlgorithm(roomId)
} }
private suspend fun preshareRoomKey(roomId: String, roomMembers: List<String>) { private suspend fun preshareRoomKey(roomId: String, roomMembers: List<String>, forceDistributeToUnverified: Boolean) {
claimMissingKeys(roomMembers) claimMissingKeys(roomMembers)
val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() } val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() }
var sharedKey = false var sharedKey = false
@ -97,7 +97,12 @@ internal class PrepareToEncryptUseCase @Inject constructor(
} }
val settings = EncryptionSettings( val settings = EncryptionSettings(
algorithm = EventEncryptionAlgorithm.MEGOLM_V1_AES_SHA2, algorithm = EventEncryptionAlgorithm.MEGOLM_V1_AES_SHA2,
onlyAllowTrustedDevices = info.blacklistUnverifiedDevices, onlyAllowTrustedDevices = if (forceDistributeToUnverified) {
false
} else {
cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
info.blacklistUnverifiedDevices
},
rotationPeriod = info.rotationPeriodMs.toULong(), rotationPeriod = info.rotationPeriodMs.toULong(),
rotationPeriodMsgs = info.rotationPeriodMsgs.toULong(), rotationPeriodMsgs = info.rotationPeriodMsgs.toULong(),
historyVisibility = if (info.shouldShareHistory) { historyVisibility = if (info.shouldShareHistory) {

View File

@ -109,7 +109,7 @@ private val loggerTag = LoggerTag("RustCryptoService", LoggerTag.CRYPTO)
@SessionScope @SessionScope
internal class RustCryptoService @Inject constructor( internal class RustCryptoService @Inject constructor(
@UserId private val userId: String, @UserId private val myUserId: String,
@DeviceId private val deviceId: String, @DeviceId private val deviceId: String,
// the crypto store // the crypto store
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@ -167,7 +167,7 @@ internal class RustCryptoService @Inject constructor(
val params = SetDeviceNameTask.Params(deviceId, deviceName) val params = SetDeviceNameTask.Params(deviceId, deviceName)
setDeviceNameTask.execute(params) setDeviceNameTask.execute(params)
try { try {
downloadKeysIfNeeded(listOf(userId), true) downloadKeysIfNeeded(listOf(myUserId), true)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device") Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device")
} }
@ -257,7 +257,7 @@ internal class RustCryptoService @Inject constructor(
setRustLogger() setRustLogger()
Timber.tag(loggerTag.value).v( Timber.tag(loggerTag.value).v(
"## CRYPTO | Successfully started up an Olm machine for " + "## CRYPTO | Successfully started up an Olm machine for " +
"$userId, $deviceId, identity keys: ${this.olmMachine.identityKeys()}" "$myUserId, $deviceId, identity keys: ${this.olmMachine.identityKeys()}"
) )
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.tag(loggerTag.value).v("Failed create an Olm machine: $throwable") Timber.tag(loggerTag.value).v("Failed create an Olm machine: $throwable")
@ -342,7 +342,7 @@ internal class RustCryptoService @Inject constructor(
} }
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> { override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
return getLiveCryptoDeviceInfo(listOf(userId)) return getLiveCryptoDeviceInfo(listOf(myUserId))
} }
override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> { override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> {
@ -350,8 +350,8 @@ internal class RustCryptoService @Inject constructor(
} }
override fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> { override fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> {
return olmMachine.getLiveDevices(listOf(userId)).map { return olmMachine.getLiveDevices(listOf(myUserId)).map {
it.filter { it.userId == userId } it.filter { it.userId == myUserId }
} }
} }
@ -609,7 +609,7 @@ internal class RustCryptoService @Inject constructor(
// Notify the our listeners about room keys so decryption is retried. // Notify the our listeners about room keys so decryption is retried.
toDeviceEvents.events.orEmpty().forEach { event -> toDeviceEvents.events.orEmpty().forEach { event ->
Timber.tag(loggerTag.value).d("Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}") Timber.tag(loggerTag.value).d("[${myUserId.take(7)}|${deviceId}] Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}")
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED) {
// rust failed to decrypt it // rust failed to decrypt it
@ -832,7 +832,7 @@ internal class RustCryptoService @Inject constructor(
* ========================================================================================== */ * ========================================================================================== */
override fun toString(): String { override fun toString(): String {
return "DefaultCryptoService of $userId ($deviceId)" return "DefaultCryptoService of $myUserId ($deviceId)"
} }
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> { override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {

View File

@ -66,8 +66,10 @@ import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.rustcomponents.sdk.crypto.OutgoingVerificationRequest import org.matrix.rustcomponents.sdk.crypto.OutgoingVerificationRequest
import org.matrix.rustcomponents.sdk.crypto.Request import org.matrix.rustcomponents.sdk.crypto.Request
@ -77,6 +79,8 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class RequestSender @Inject constructor( internal class RequestSender @Inject constructor(
@UserId
private val myUserId: String,
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
@ -94,8 +98,9 @@ internal class RequestSender @Inject constructor(
private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
private val getRoomSessionDataTask: GetRoomSessionDataTask, private val getRoomSessionDataTask: GetRoomSessionDataTask,
private val moshi: Moshi, private val moshi: Moshi,
private val cryptoCoroutineScope: CoroutineScope, cryptoCoroutineScope: CoroutineScope,
private val rateLimiter: PerSessionBackupQueryRateLimiter, private val rateLimiter: PerSessionBackupQueryRateLimiter,
private val localEchoRepository: LocalEchoRepository
) { ) {
private val scope = CoroutineScope( private val scope = CoroutineScope(
@ -136,7 +141,13 @@ internal class RequestSender @Inject constructor(
} }
private suspend fun sendRoomMessage(request: OutgoingVerificationRequest.InRoom, retryCount: Int): SendResponse { private suspend fun sendRoomMessage(request: OutgoingVerificationRequest.InRoom, retryCount: Int): SendResponse {
return sendRoomMessage(request.eventType, request.roomId, request.content, request.requestId, retryCount) return sendRoomMessage(
eventType = request.eventType,
roomId = request.roomId,
content = request.content,
transactionId = request.requestId,
retryCount = retryCount
)
} }
suspend fun sendRoomMessage(request: Request.RoomMessage, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT): String { suspend fun sendRoomMessage(request: Request.RoomMessage, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT): String {
@ -153,7 +164,13 @@ internal class RequestSender @Inject constructor(
): SendResponse { ): SendResponse {
val paramsAdapter = moshi.adapter<Content>(Map::class.java) val paramsAdapter = moshi.adapter<Content>(Map::class.java)
val jsonContent = paramsAdapter.fromJson(content) val jsonContent = paramsAdapter.fromJson(content)
val event = Event(eventType, transactionId, jsonContent, roomId = roomId) val event = Event(
senderId = myUserId,
type = eventType,
eventId = transactionId,
content = jsonContent,
roomId = roomId)
localEchoRepository.createLocalEcho(event)
val params = SendVerificationMessageTask.Params(event, retryCount) val params = SendVerificationMessageTask.Params(event, retryCount)
return sendVerificationMessageTask.get().execute(params) return sendVerificationMessageTask.get().execute(params)
} }

View File

@ -80,19 +80,25 @@ internal class RustVerificationService @Inject constructor(
* All verification related events should be forwarded through this method to * All verification related events should be forwarded through this method to
* the verification service. * the verification service.
* *
* If the verification event is not encrypted it should be provided to the olmMachine. * This method mainly just fetches the appropriate rust object that will be created or updated by the event and
* Otherwise events are at this point already handled by the rust-sdk through the receival
* of the to-device events and the decryption of room events. In this case this method mainly just
* fetches the appropriate rust object that will be created or updated by the event and
* dispatches updates to our listeners. * dispatches updates to our listeners.
*/ */
internal suspend fun onEvent(roomId: String?, event: Event) { internal suspend fun onEvent(roomId: String?, event: Event) {
if (roomId != null && !event.isEncrypted()) { if (roomId != null && event.unsignedData?.transactionId == null) {
if (isVerificationEvent(event)) { if (isVerificationEvent(event)) {
try { try {
olmMachine.receiveUnencryptedVerificationEvent(roomId, event) val clearEvent = if (event.isEncrypted()) {
event.copy(
content = event.getDecryptedContent(),
type = event.getDecryptedType(),
roomId = roomId
)
} else {
event
}
olmMachine.receiveVerificationEvent(roomId, clearEvent)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.w(failure, "Failed to receiveUnencryptedVerificationEvent") Timber.w(failure, "Failed to receiveUnencryptedVerificationEvent ${failure.message}")
} }
} }
} }
@ -111,8 +117,8 @@ internal class RustVerificationService @Inject constructor(
} }
private fun isVerificationEvent(event: Event): Boolean { private fun isVerificationEvent(event: Event): Boolean {
val eventType = event.type ?: return false val eventType = event.getClearType()
val eventContent = event.content ?: return false val eventContent = event.getClearContent() ?: return false
return EventType.isVerificationEvent(eventType) || return EventType.isVerificationEvent(eventType) ||
(eventType == EventType.MESSAGE && (eventType == EventType.MESSAGE &&
eventContent[MessageContent.MSG_TYPE_JSON_KEY] == MessageType.MSGTYPE_VERIFICATION_REQUEST) eventContent[MessageContent.MSG_TYPE_JSON_KEY] == MessageType.MSGTYPE_VERIFICATION_REQUEST)
@ -127,22 +133,23 @@ internal class RustVerificationService @Inject constructor(
/** Dispatch updates after a verification event has been received */ /** Dispatch updates after a verification event has been received */
private suspend fun onUpdate(event: Event) { private suspend fun onUpdate(event: Event) {
Timber.v("[${olmMachine.userId().take(6)}] Verification on event ${event.getClearType()}")
val sender = event.senderId ?: return val sender = event.senderId ?: return
val flowId = getFlowId(event) ?: return val flowId = getFlowId(event) ?: return Unit.also {
Timber.w("onUpdate for unknown flowId senderId ${event.getClearType()}")
}
val verificationRequest = olmMachine.getVerificationRequest(sender, flowId) val verificationRequest = olmMachine.getVerificationRequest(sender, flowId)
if (event.getClearType() == EventType.KEY_VERIFICATION_READY) { if (event.getClearType() == EventType.KEY_VERIFICATION_READY) {
// we start the qr here in order to display the code
verificationRequest?.startQrCode() verificationRequest?.startQrCode()
} }
verificationRequest?.dispatchRequestUpdated()
val verification = getExistingTransaction(sender, flowId) ?: return
verificationListenersHolder.dispatchTxUpdated(verification)
} }
/** Check if the start event created new verification objects and dispatch updates */ /** Check if the start event created new verification objects and dispatch updates */
private suspend fun onStart(event: Event) { private suspend fun onStart(event: Event) {
if (event.unsignedData?.transactionId != null) return // remote echo if (event.unsignedData?.transactionId != null) return // remote echo
Timber.w("VALR onStart $event") Timber.w("VALR onStart ${event.eventId}")
val sender = event.senderId ?: return val sender = event.senderId ?: return
val flowId = getFlowId(event) ?: return val flowId = getFlowId(event) ?: return
@ -167,7 +174,6 @@ internal class RustVerificationService @Inject constructor(
Timber.d("## Verification: start for $sender") Timber.d("## Verification: start for $sender")
// update the request as the start updates it's state // update the request as the start updates it's state
request.dispatchRequestUpdated()
verificationListenersHolder.dispatchTxUpdated(transaction) verificationListenersHolder.dispatchTxUpdated(transaction)
} else { } else {
// This didn't originate from a request, so tell our listeners that // This didn't originate from a request, so tell our listeners that
@ -187,19 +193,11 @@ internal class RustVerificationService @Inject constructor(
event.getClearContent().toModel<ToDeviceVerificationEvent>()?.transactionId event.getClearContent().toModel<ToDeviceVerificationEvent>()?.transactionId
} ?: return } ?: return
val sender = event.senderId ?: return val sender = event.senderId ?: return
val request = getExistingVerificationRequest(sender, flowId) ?: return val request = olmMachine.getVerificationRequest(sender, flowId) ?: return
verificationListenersHolder.dispatchRequestAdded(request) verificationListenersHolder.dispatchRequestAdded(request)
} }
// override fun addListener(listener: VerificationService.Listener) {
// verificationListenersHolder.addListener(listener)
// }
//
// override fun removeListener(listener: VerificationService.Listener) {
// verificationListenersHolder.removeListener(listener)
// }
override suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { override suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
olmMachine.getDevice(userId, deviceID)?.markAsTrusted() olmMachine.getDevice(userId, deviceID)?.markAsTrusted()
} }
@ -269,14 +267,14 @@ internal class RustVerificationService @Inject constructor(
roomId: String, roomId: String,
localId: String? localId: String?
): PendingVerificationRequest { ): PendingVerificationRequest {
olmMachine.ensureUsersKeys(listOf(otherUserId)) Timber.w("verification: requestKeyVerificationInDMs in room $roomId with $otherUserId")
olmMachine.ensureUsersKeys(listOf(otherUserId), true)
val verification = when (val identity = olmMachine.getIdentity(otherUserId)) { val verification = when (val identity = olmMachine.getIdentity(otherUserId)) {
is UserIdentity -> identity.requestVerification(methods, roomId, localId!!) is UserIdentity -> identity.requestVerification(methods, roomId, localId!!)
is OwnUserIdentity -> throw IllegalArgumentException("This method doesn't support verification of our own user") is OwnUserIdentity -> throw IllegalArgumentException("This method doesn't support verification of our own user")
null -> throw IllegalArgumentException("The user that we wish to verify doesn't support cross signing") null -> throw IllegalArgumentException("The user that we wish to verify doesn't support cross signing")
} }
Timber.w("##VALR requestKeyVerificationInDMs ${verification.flowId()} > $verification")
return verification.toPendingVerificationRequest() return verification.toPendingVerificationRequest()
} }
@ -319,13 +317,15 @@ internal class RustVerificationService @Inject constructor(
override suspend fun startKeyVerification(method: VerificationMethod, otherUserId: String, requestId: String): String? { override suspend fun startKeyVerification(method: VerificationMethod, otherUserId: String, requestId: String): String? {
return if (method == VerificationMethod.SAS) { return if (method == VerificationMethod.SAS) {
val request = olmMachine.getVerificationRequest(otherUserId, requestId) val request = olmMachine.getVerificationRequest(otherUserId, requestId)
?: throw IllegalArgumentException("Unknown request with id: $requestId")
val sas = request?.startSasVerification() val sas = request.startSasVerification()
if (sas != null) { if (sas != null) {
verificationListenersHolder.dispatchTxAdded(sas) verificationListenersHolder.dispatchTxAdded(sas)
sas.transactionId sas.transactionId
} else { } else {
Timber.w("Failed to start verification with method $method")
null null
} }
} else { } else {
@ -338,7 +338,6 @@ internal class RustVerificationService @Inject constructor(
?: return null ?: return null
val qrVerification = matchingRequest.scanQrCode(scannedData) val qrVerification = matchingRequest.scanQrCode(scannedData)
?: return null ?: return null
verificationListenersHolder.dispatchRequestUpdated(matchingRequest.toPendingVerificationRequest())
verificationListenersHolder.dispatchTxAdded(qrVerification) verificationListenersHolder.dispatchTxAdded(qrVerification)
return qrVerification.transactionId return qrVerification.transactionId
} }

View File

@ -83,20 +83,6 @@ internal class SasVerification @AssistedInject constructor(
SasState.Done -> SasTransactionState.Done(true) SasState.Done -> SasTransactionState.Done(true)
is SasState.Cancelled -> SasTransactionState.Cancelled(safeValueOf(state.cancelInfo.cancelCode), state.cancelInfo.cancelledByUs) is SasState.Cancelled -> SasTransactionState.Cancelled(safeValueOf(state.cancelInfo.cancelCode), state.cancelInfo.cancelledByUs)
} }
// refreshData()
// val cancelInfo = inner.cancelInfo
//
// return when {
// cancelInfo != null -> {
// val cancelCode = safeValueOf(cancelInfo.cancelCode)
// SasTransactionState.Cancelled(cancelCode, cancelInfo.cancelledByUs)
// }
// inner.isDone -> SasTransactionState.Done(true)
// inner.haveWeConfirmed -> SasTransactionState.SasAccepted
// inner.canBePresented -> SasTransactionState.SasShortCodeReady
// inner.hasBeenAccepted -> SasTransactionState.SasAccepted
// else -> SasTransactionState.SasStarted
// }
} }
/** Get the unique id of this verification */ /** Get the unique id of this verification */

View File

@ -38,17 +38,17 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.network.RequestSender
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.rustcomponents.sdk.crypto.VerificationRequestListener
import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState
import timber.log.Timber
import org.matrix.rustcomponents.sdk.crypto.VerificationRequest as InnerVerificationRequest import org.matrix.rustcomponents.sdk.crypto.VerificationRequest as InnerVerificationRequest
fun InnerVerificationRequest.dbgString(): String { fun InnerVerificationRequest.dbgString(): String {
val that = this val that = this
return buildString { return buildString {
append("InnerVerificationRequest(") append("(")
append("isDone=${that.isDone()},") append("flowId=${that.flowId()}")
append("isReady=${that.isReady()},") append("state=${that.state()},")
append("isPassive=${that.isPassive()},")
append("weStarted=${that.weStarted()},")
append("isCancelled=${that.isCancelled()}")
append(")") append(")")
} }
} }
@ -69,7 +69,7 @@ internal class VerificationRequest @AssistedInject constructor(
private val sasVerificationFactory: SasVerification.Factory, private val sasVerificationFactory: SasVerification.Factory,
private val qrCodeVerificationFactory: QrCodeVerification.Factory, private val qrCodeVerificationFactory: QrCodeVerification.Factory,
private val clock: Clock, private val clock: Clock,
) { ) : VerificationRequestListener {
private val innerOlmMachine = olmMachine.inner() private val innerOlmMachine = olmMachine.inner()
@ -78,14 +78,18 @@ internal class VerificationRequest @AssistedInject constructor(
fun create(innerVerificationRequest: InnerVerificationRequest): VerificationRequest fun create(innerVerificationRequest: InnerVerificationRequest): VerificationRequest
} }
init {
innerVerificationRequest.setChangesListener(this)
}
fun startQrCode() { fun startQrCode() {
innerVerificationRequest.startQrVerification() innerVerificationRequest.startQrVerification()
} }
internal fun dispatchRequestUpdated() { // internal fun dispatchRequestUpdated() {
val tx = toPendingVerificationRequest() // val tx = toPendingVerificationRequest()
verificationListenersHolder.dispatchRequestUpdated(tx) // verificationListenersHolder.dispatchRequestUpdated(tx)
} // }
/** Get the flow ID of this verification request /** Get the flow ID of this verification request
* *
@ -97,6 +101,8 @@ internal class VerificationRequest @AssistedInject constructor(
return innerVerificationRequest.flowId() return innerVerificationRequest.flowId()
} }
fun innerState() = innerVerificationRequest.state()
/** The user ID of the other user that is participating in this verification flow */ /** The user ID of the other user that is participating in this verification flow */
internal fun otherUser(): String { internal fun otherUser(): String {
return innerVerificationRequest.otherUserId() return innerVerificationRequest.otherUserId()
@ -108,7 +114,6 @@ internal class VerificationRequest @AssistedInject constructor(
* didn't yet accept the verification flow. * didn't yet accept the verification flow.
* */ * */
internal fun otherDeviceId(): String? { internal fun otherDeviceId(): String? {
refreshData()
return innerVerificationRequest.otherDeviceId() return innerVerificationRequest.otherDeviceId()
} }
@ -132,13 +137,11 @@ internal class VerificationRequest @AssistedInject constructor(
* verification. * verification.
*/ */
internal fun isReady(): Boolean { internal fun isReady(): Boolean {
refreshData()
return innerVerificationRequest.isReady() return innerVerificationRequest.isReady()
} }
/** Did we advertise that we're able to scan QR codes */ /** Did we advertise that we're able to scan QR codes */
internal fun canScanQrCodes(): Boolean { internal fun canScanQrCodes(): Boolean {
refreshData()
return innerVerificationRequest.ourSupportedMethods()?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false return innerVerificationRequest.ourSupportedMethods()?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false
} }
@ -161,12 +164,7 @@ internal class VerificationRequest @AssistedInject constructor(
val request = innerVerificationRequest.accept(stringMethods) val request = innerVerificationRequest.accept(stringMethods)
?: return // should throw here? ?: return // should throw here?
try { try {
dispatchRequestUpdated()
requestSender.sendVerificationRequest(request) requestSender.sendVerificationRequest(request)
// if (innerVerificationRequest.isReady()) {
// activeQRCode = innerVerificationRequest.startQrVerification()
// }
} catch (failure: Throwable) { } catch (failure: Throwable) {
cancel() cancel()
} }
@ -190,9 +188,9 @@ internal class VerificationRequest @AssistedInject constructor(
internal suspend fun startSasVerification(): SasVerification? { internal suspend fun startSasVerification(): SasVerification? {
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
val result = innerVerificationRequest.startSasVerification() val result = innerVerificationRequest.startSasVerification()
?: return@withContext null ?: return@withContext null.also {
// sasStartResult.request Timber.w("Failed to start verification")
// val result = innerOlmMachine.startSasVerification(innerVerificationRequest.otherUserId, innerVerificationRequest.flowId) ?: return@withContext null }
try { try {
requestSender.sendVerificationRequest(result.request) requestSender.sendVerificationRequest(result.request)
sasVerificationFactory.create(result.sas) sasVerificationFactory.create(result.sas)
@ -242,69 +240,92 @@ internal class VerificationRequest @AssistedInject constructor(
* The method turns into a noop, if the verification flow has already been cancelled. * The method turns into a noop, if the verification flow has already been cancelled.
*/ */
internal suspend fun cancel() = withContext(NonCancellable) { internal suspend fun cancel() = withContext(NonCancellable) {
// TODO damir how to add the code?
val request = innerVerificationRequest.cancel() ?: return@withContext val request = innerVerificationRequest.cancel() ?: return@withContext
dispatchRequestUpdated()
tryOrNull("Fail to send cancel request") { tryOrNull("Fail to send cancel request") {
requestSender.sendVerificationRequest(request, retryCount = Int.MAX_VALUE) requestSender.sendVerificationRequest(request, retryCount = Int.MAX_VALUE)
} }
} }
/** Fetch fresh data from the Rust side for our verification flow */
private fun refreshData() {
val request = innerOlmMachine.getVerificationRequest(innerVerificationRequest.otherUserId(), innerVerificationRequest.flowId())
if (request != null) {
innerVerificationRequest = request
}
}
private fun state(): EVerificationState { private fun state(): EVerificationState {
if (innerVerificationRequest.isCancelled()) { Timber.v("Verification state() ${innerVerificationRequest.state()}")
return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) { when (innerVerificationRequest.state()) {
EVerificationState.HandledByOtherSession VerificationRequestState.Requested -> {
} else { return if (weStarted()) {
EVerificationState.Cancelled EVerificationState.WaitingForReady
}
}
if (innerVerificationRequest.isPassive()) {
return EVerificationState.HandledByOtherSession
}
if (innerVerificationRequest.isDone()) {
return EVerificationState.Done
}
val started = innerOlmMachine.getVerification(otherUser(), flowId())
if (started != null) {
val asSas = started.asSas()
if (asSas != null) {
return if (asSas.weStarted()) {
EVerificationState.WeStarted
} else { } else {
EVerificationState.Started EVerificationState.Requested
} }
} }
val asQR = started.asQr() is VerificationRequestState.Ready -> {
if (asQR != null) { val started = innerOlmMachine.getVerification(otherUser(), flowId())
// Timber.w("VALR: weStarted ${asQR.weStarted()}") if (started != null) {
// Timber.w("VALR: reciprocated ${asQR.reciprocated()}") val asSas = started.asSas()
// Timber.w("VALR: isDone ${asQR.isDone()}") if (asSas != null) {
// Timber.w("VALR: hasBeenScanned ${asQR.hasBeenScanned()}") return if (asSas.weStarted()) {
if (asQR.reciprocated() || asQR.hasBeenScanned()) { EVerificationState.WeStarted
return if (weStarted()) { } else {
EVerificationState.WeStarted EVerificationState.Started
} else EVerificationState.Started }
}
val asQR = started.asQr()
if (asQR != null) {
if (asQR.reciprocated() || asQR.hasBeenScanned()) {
return if (weStarted()) {
EVerificationState.WeStarted
} else EVerificationState.Started
}
}
}
return EVerificationState.Ready
}
VerificationRequestState.Done -> {
return EVerificationState.Done
}
is VerificationRequestState.Cancelled -> {
return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) {
EVerificationState.HandledByOtherSession
} else {
EVerificationState.Cancelled
} }
} }
} }
if (innerVerificationRequest.isReady()) { //
return EVerificationState.Ready // if (innerVerificationRequest.isCancelled()) {
} // return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) {
return if (weStarted()) { // EVerificationState.HandledByOtherSession
EVerificationState.WaitingForReady // } else {
} else { // EVerificationState.Cancelled
EVerificationState.Requested // }
} // }
// if (innerVerificationRequest.isPassive()) {
// return EVerificationState.HandledByOtherSession
// }
// if (innerVerificationRequest.isDone()) {
// return EVerificationState.Done
// }
//
// val started = innerOlmMachine.getVerification(otherUser(), flowId())
// if (started != null) {
// val asSas = started.asSas()
// if (asSas != null) {
// return if (asSas.weStarted()) {
// EVerificationState.WeStarted
// } else {
// EVerificationState.Started
// }
// }
// val asQR = started.asQr()
// if (asQR != null) {
// if (asQR.reciprocated() || asQR.hasBeenScanned()) {
// return if (weStarted()) {
// EVerificationState.WeStarted
// } else EVerificationState.Started
// }
// }
// }
// if (innerVerificationRequest.isReady()) {
// return EVerificationState.Ready
// }
} }
/** Convert the VerificationRequest into a PendingVerificationRequest /** Convert the VerificationRequest into a PendingVerificationRequest
@ -317,7 +338,6 @@ internal class VerificationRequest @AssistedInject constructor(
* @return The PendingVerificationRequest that matches data from this VerificationRequest. * @return The PendingVerificationRequest that matches data from this VerificationRequest.
*/ */
internal fun toPendingVerificationRequest(): PendingVerificationRequest { internal fun toPendingVerificationRequest(): PendingVerificationRequest {
refreshData()
val cancelInfo = innerVerificationRequest.cancelInfo() val cancelInfo = innerVerificationRequest.cancelInfo()
val cancelCode = val cancelCode =
if (cancelInfo != null) { if (cancelInfo != null) {
@ -364,6 +384,10 @@ internal class VerificationRequest @AssistedInject constructor(
} }
} }
override fun onChange(state: VerificationRequestState) {
verificationListenersHolder.dispatchRequestUpdated(this)
}
override fun toString(): String { override fun toString(): String {
return super.toString() + "\n${innerVerificationRequest.dbgString()}" return super.toString() + "\n${innerVerificationRequest.dbgString()}"
} }

View File

@ -331,22 +331,28 @@ class UserVerificationViewModel @AssistedInject constructor(
val roomId = session.roomService().getExistingDirectRoomWithUser(initialState.otherUserId) val roomId = session.roomService().getExistingDirectRoomWithUser(initialState.otherUserId)
?: session.roomService().createDirectRoom(initialState.otherUserId) ?: session.roomService().createDirectRoom(initialState.otherUserId)
val request = session.cryptoService().verificationService() try {
.requestKeyVerificationInDMs( val request = session.cryptoService().verificationService()
supportedVerificationMethodsProvider.provide(), .requestKeyVerificationInDMs(
initialState.otherUserId, methods = supportedVerificationMethodsProvider.provide(),
roomId, otherUserId = initialState.otherUserId,
roomId = roomId,
)
currentTransactionId = request.transactionId
setState {
copy(
pendingRequest = Success(request),
transactionId = request.transactionId
) )
}
currentTransactionId = request.transactionId } catch (failure: Throwable) {
setState {
Timber.w("VALR started request is $request") copy(
pendingRequest = Fail(failure),
setState { )
copy( }
pendingRequest = Success(request),
transactionId = request.transactionId
)
} }
} }
} }