diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 669e27edfd..2dbc5c99db 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -125,21 +125,21 @@ class FlowSession(private val session: Session) { } fun liveUserCryptoDevices(userId: String): Flow> { - return session.cryptoService().getLiveCryptoDeviceInfo(userId).asFlow() + return session.cryptoService().getLiveCryptoDeviceInfoList(userId) .startWith(session.coroutineDispatchers.io) { - session.cryptoService().getCryptoDeviceInfo(userId) + session.cryptoService().getCryptoDeviceInfoList(userId) } } fun liveCrossSigningInfo(userId: String): Flow> { - return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asFlow() + return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId) .startWith(session.coroutineDispatchers.io) { session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional() } } fun liveCrossSigningPrivateKeys(): Flow> { - return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asFlow() + return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys() .startWith(session.coroutineDispatchers.io) { session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b1b7986bf5..1334b83191 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -110,6 +110,9 @@ dependencies { implementation libs.jetbrains.coroutinesAndroid implementation 'org.matrix.rustcomponents:crypto-android:0.1.1-SNAPSHOT' + //implementation files('libs/crypto-android-release.aar') + +// implementation(name: 'crypto-android-release', ext: 'aar') implementation 'net.java.dev.jna:jna:5.10.0@aar' implementation libs.androidx.appCompat diff --git a/matrix-sdk-android/libs/crypto-android-release.aar b/matrix-sdk-android/libs/crypto-android-release.aar new file mode 100644 index 0000000000..d2e9c800a9 Binary files /dev/null and b/matrix-sdk-android/libs/crypto-android-release.aar differ diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 031d0a8bcf..ab6fadb52b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -378,7 +378,7 @@ class CommonTestHelper(context: Context) { assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) } - suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { + suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: suspend (() -> Boolean)) { while (true) { delay(1000) if (condition()) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 71796192a8..1dd223648b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.common import android.os.SystemClock import android.util.Log import androidx.lifecycle.Observer +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -29,8 +30,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +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.VerificationTxState import org.matrix.android.sdk.api.session.events.model.Event @@ -275,7 +275,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { } fun initializeCrossSigning(session: Session) { - testHelper.doSync { + testHelper.runBlockingTest { session.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -288,7 +288,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { ) ) } - }, it) + }) } } @@ -300,24 +300,24 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { val aliceVerificationService = alice.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService() - aliceVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID, - roomId, - bob.myUserId, - bob.sessionParams.credentials.deviceId!!) + runBlocking { + aliceVerificationService.beginKeyVerification( + VerificationMethod.SAS, + roomId, + bob.myUserId,) + } // we should reach SHOW SAS on both - var alicePovTx: OutgoingSasVerificationTransaction? = null - var bobPovTx: IncomingSasVerificationTransaction? = null + var alicePovTx: SasVerificationTransaction? = null + var bobPovTx: SasVerificationTransaction? = null // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${bobPovTx?.state}") if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() + bobPovTx?.acceptVerification() true } else { false @@ -327,18 +327,18 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction - Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.state}") alicePovTx?.state == VerificationTxState.ShortCodeReady } } // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${bobPovTx?.state}") if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() + bobPovTx?.acceptVerification() } bobPovTx?.state == VerificationTxState.ShortCodeReady } @@ -346,8 +346,10 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) - bobPovTx!!.userHasVerifiedShortCode() - alicePovTx!!.userHasVerifiedShortCode() + runBlocking { + bobPovTx!!.userHasVerifiedShortCode() + alicePovTx!!.userHasVerifiedShortCode() + } testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index a7a81bacf5..d48881e1b0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -60,8 +60,8 @@ class PreShareKeysTest : InstrumentedTest { Log.d("#Test", "Room Key Received from alice $preShareCount") // Force presharing of new outbound key - testHelper.doSync { - aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) + testHelper.runBlockingTest { + aliceSession.cryptoService().prepareToEncrypt(e2eRoomID) } testHelper.waitWithLatch { latch -> diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 0a8ce67680..8226e38fc3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -144,9 +144,12 @@ class UnwedgingTest : InstrumentedTest { // - Store the olm session between A&B devices // Let us pickle our session with bob here so we can later unpickle it // and wedge our session. - val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) + var myDevice = testHelper.runBlockingTest { + bobSession.cryptoService().getMyCryptoDevice() + } + val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(myDevice.identityKey()!!) sessionIdsForBob!!.size shouldBe 1 - val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! + val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), myDevice.identityKey()!!)!! val oldSession = serializeForRealm(olmSession.olmSession) @@ -174,7 +177,10 @@ class UnwedgingTest : InstrumentedTest { // Let us wedge the session now. Set crypto state like after the first message Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") - aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!) + myDevice = testHelper.runBlockingTest { + bobSession.cryptoService().getMyCryptoDevice() + } + aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), myDevice.identityKey()!!) Thread.sleep(6_000) // Force new session, and key share @@ -207,7 +213,7 @@ class UnwedgingTest : InstrumentedTest { bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) // It's a trick to force key request on fail to decrypt - testHelper.doSync { + testHelper.runBlockingTest { bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -220,7 +226,7 @@ class UnwedgingTest : InstrumentedTest { ) ) } - }, it) + }) } // Wait until we received back the key diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index b4c8f1a22a..ef49e67029 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -53,7 +54,7 @@ class XSigningTest : InstrumentedTest { fun test_InitializeAndStoreKeys() { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - testHelper.doSync { + testHelper.runBlockingTest { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { @@ -65,10 +66,12 @@ class XSigningTest : InstrumentedTest { ) ) } - }, it) + }) } - val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + val myCrossSigningKeys = testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + } val masterPubKey = myCrossSigningKeys?.masterKey() assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) val selfSigningKey = myCrossSigningKeys?.selfSigningKey() @@ -78,7 +81,10 @@ class XSigningTest : InstrumentedTest { assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) - assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + val userTrustResult = testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId) + } + assertTrue("Signing Keys should be trusted", userTrustResult.isVerified()) testHelper.signOutAndClose(aliceSession) } @@ -99,29 +105,37 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { + testHelper.runBlockingTest { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(aliceAuthParams) } - }, it) + }) + } + testHelper.runBlockingTest { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) } - testHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) } // Check that alice can see bob keys testHelper.runBlockingTest { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true) } - val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + val bobKeysFromAlicePOV = testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + } assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) - assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + val myKeys = testHelper.runBlockingTest { + bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + } + + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, myKeys?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, myKeys?.selfSigningKey()?.unpaddedBase64PublicKey) assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) @@ -145,25 +159,33 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(aliceAuthParams) - } - }, it) } - testHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) } + testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }) + } + testHelper.runBlockingTest { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) + } // Check that alice can see bob keys val bobUserId = bobSession.myUserId testHelper.runBlockingTest { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true) } - val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) + val bobKeysFromAlicePOV = testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) + } assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - testHelper.doSync { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().trustUser(bobUserId) + } // Now bobs logs in on a new device and verifies it // We will want to test that in alice POV, this new device would be trusted by cross signing @@ -180,7 +202,9 @@ class XSigningTest : InstrumentedTest { fail("Bob should see the new device") } - val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId) + val bobSecondDevicePOVFirstDevice = runBlocking { + bobSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobSecondDeviceId) + } assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session @@ -198,7 +222,9 @@ class XSigningTest : InstrumentedTest { fail("Alice should see the new device") } - val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + val result = testHelper.runBlockingTest { + aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + } assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) testHelper.signOutAndClose(aliceSession) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 7b24f28e06..c83549f9dd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -23,6 +23,7 @@ import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail +import kotlinx.coroutines.delay import org.junit.Assert import org.junit.FixMethodOrder import org.junit.Ignore @@ -35,7 +36,6 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction 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.VerificationService @@ -50,7 +50,6 @@ import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.GossipingRequestState import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState -import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -93,7 +92,9 @@ class KeyShareTests : InstrumentedTest { assert(receivedEvent!!.isEncrypted()) try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } fail("should fail") } catch (failure: Throwable) { } @@ -106,7 +107,7 @@ class KeyShareTests : InstrumentedTest { var outGoingRequestId: String? = null - commonTestHelper.waitWithLatch { latch -> + commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession2.cryptoService().getOutgoingRoomKeyRequests() .filter { req -> @@ -148,14 +149,17 @@ class KeyShareTests : InstrumentedTest { } try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } fail("should fail") } catch (failure: Throwable) { } // Mark the device as trusted - aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.deviceId ?: "") + commonTestHelper.runBlockingTest { + aliceSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession2.sessionParams.deviceId ?: "") + } // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -185,7 +189,9 @@ class KeyShareTests : InstrumentedTest { } try { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + commonTestHelper.runBlockingTest { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") + } } catch (failure: Throwable) { fail("should have been able to decrypt") } @@ -199,7 +205,7 @@ class KeyShareTests : InstrumentedTest { fun test_ShareSSSSSecret() { val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - commonTestHelper.doSync { + commonTestHelper.runBlockingTest { aliceSession1.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -211,7 +217,7 @@ class KeyShareTests : InstrumentedTest { ) ) } - }, it) + }) } // Also bootstrap keybackup on first session @@ -242,27 +248,30 @@ class KeyShareTests : InstrumentedTest { aliceVerificationService1.addListener(object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { + if (tx !is SasVerificationTransaction) return Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.OnStarted) { - (tx as IncomingSasVerificationTransaction).performAccept() + when (tx.state) { + VerificationTxState.OnStarted -> commonTestHelper.runBlockingTest { + tx.acceptVerification() } - if (tx.state == VerificationTxState.ShortCodeReady) { + VerificationTxState.ShortCodeReady -> commonTestHelper.runBlockingTest { session1ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) + delay(500) tx.userHasVerifiedShortCode() } } } - }) + } + ) aliceVerificationService2.addListener(object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { + if (tx !is SasVerificationTransaction) return Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.ShortCodeReady) { + when (tx.state) { + VerificationTxState.ShortCodeReady -> commonTestHelper.runBlockingTest { session2ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) + delay(500) tx.userHasVerifiedShortCode() } } @@ -270,12 +279,13 @@ class KeyShareTests : InstrumentedTest { }) val txId = "m.testVerif12" - aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId - ?: "", txId) + commonTestHelper.runBlockingTest { + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, txId) + } commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true + aliceSession1.cryptoService().getCryptoDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true } } @@ -312,7 +322,7 @@ class KeyShareTests : InstrumentedTest { fun test_ImproperKeyShareBug() { val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - commonTestHelper.doSync { + commonTestHelper.runBlockingTest { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -325,7 +335,7 @@ class KeyShareTests : InstrumentedTest { ) ) } - }, it) + }) } // Create an encrypted room and send a couple of messages @@ -346,7 +356,7 @@ class KeyShareTests : InstrumentedTest { // Create bob session val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - commonTestHelper.doSync { + commonTestHelper.runBlockingTest { bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -359,7 +369,7 @@ class KeyShareTests : InstrumentedTest { ) ) } - }, it) + }) } // Let alice invite bob @@ -380,7 +390,10 @@ class KeyShareTests : InstrumentedTest { val roomRoomBobPov = aliceSession.getRoom(roomId) val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId) - var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") } + var dRes = + commonTestHelper.runBlockingTest { + tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") } + } assert(dRes == null) @@ -391,7 +404,9 @@ class KeyShareTests : InstrumentedTest { Thread.sleep(3_000) // With the bug the first session would have improperly reshare that key :/ - dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") } + dRes = commonTestHelper.runBlockingTest { + tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") } + } Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}") assert(dRes?.clearEvent == null) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 2e24373526..4ff556895b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.FixMethodOrder import org.junit.Ignore @@ -92,7 +93,9 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld try { // .. might need to wait a bit for stability? - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + runBlocking { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -117,7 +120,9 @@ class WithHeldTests : InstrumentedTest { // Previous message should still be undecryptable (partially withheld session) try { // .. might need to wait a bit for stability? - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + runBlocking { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -164,7 +169,9 @@ class WithHeldTests : InstrumentedTest { val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) try { // .. might need to wait a bit for stability? - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + runBlocking { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + } Assert.fail("This session should not be able to decrypt") } catch (failure: Throwable) { val type = (failure as MXCryptoError.Base).errorType @@ -222,7 +229,7 @@ class WithHeldTests : InstrumentedTest { cryptoTestHelper.initializeCrossSigning(bobSecondSession) // Trust bob second device from Alice POV - mTestHelper.runBlockingTest { + testHelper.runBlockingTest { aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!) bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index c9e319ad6a..ac0a8e23d8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -38,7 +39,6 @@ import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP -import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import java.util.concurrent.CountDownLatch @@ -164,17 +164,22 @@ class KeysBackupTest : InstrumentedTest { val latch = CountDownLatch(1) - assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) - assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) - + runBlocking { + assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) + assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) + } val stateObserver = StateObserver(keysBackup, latch, 5) keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) testHelper.await(latch) - val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) - val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + val nbOfKeys = runBlocking { + cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) + } + val backedUpKeys = runBlocking { + cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) + } assertEquals(2, nbOfKeys) assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) @@ -833,9 +838,12 @@ class KeysBackupTest : InstrumentedTest { assertEquals(1, keysBackupVersionTrust.signatures.size) val signature = keysBackupVersionTrust.signatures[0] + val device = runBlocking { + cryptoTestData.firstSession.cryptoService().getMyCryptoDevice() + } assertTrue(signature.valid) assertNotNull(signature.device) - assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) + assertEquals(device.deviceId, signature.deviceId) assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) @@ -1056,7 +1064,9 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup2.isEnabled) // - Validate the old device from the new one - aliceSession2.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession2.myUserId, oldDeviceId) + testHelper.runBlockingTest { + aliceSession2.cryptoService().verificationService().markedLocallyAsManuallyVerified(aliceSession2.myUserId, oldDeviceId) + } // -> Backup should automatically enable on the new device val latch4 = CountDownLatch(1) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index c5666ca2db..453de38d89 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -80,7 +80,10 @@ class KeysBackupTestHelper( } } - Assert.assertEquals(2, cryptoTestData.firstSession.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys()) + val totalNumbersOfBackedUpKeys = testHelper.runBlockingTest { + cryptoTestData.firstSession.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys() + } + Assert.assertEquals(2, totalNumbersOfBackedUpKeys) val aliceUserId = cryptoTestData.firstSession.myUserId @@ -88,15 +91,21 @@ class KeysBackupTestHelper( val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) // Test check: aliceSession2 has no keys at login - Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + val inboundGroupSessionCount = testHelper.runBlockingTest { + aliceSession2.cryptoService().inboundGroupSessionsCount(false) + } + Assert.assertEquals(0, inboundGroupSessionCount) // Wait for backup state to be NotTrusted waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) stateObserver.stopAndCheckStates(null) + val totalNumbersOfBackedUpKeysFromNewSession = testHelper.runBlockingTest { + aliceSession2.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys() + } return KeysBackupScenarioData(cryptoTestData, - aliceSession2.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys(), + totalNumbersOfBackedUpKeysFromNewSession, prepareKeysBackupDataResult, aliceSession2) } @@ -182,7 +191,10 @@ class KeysBackupTestHelper( Assert.assertEquals(total, imported) // - The new device must have the same count of megolm keys - Assert.assertEquals(testData.aliceKeys, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + val inboundGroupSessionCount = testHelper.runBlockingTest { + testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false) + } + Assert.assertEquals(testData.aliceKeys, inboundGroupSessionCount) // - Alice must have the same keys on both devices // diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index 8cd725504d..00352a1164 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.verification import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder @@ -32,9 +32,7 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasMode +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest 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.VerificationService @@ -49,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import timber.log.Timber import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @@ -75,10 +74,13 @@ class SASTest : InstrumentedTest { } bobVerificationService.addListener(bobListener) - val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, - bobSession.myUserId, - bobSession.cryptoService().getMyDevice().deviceId, - null) + val bobDevice = testHelper.runBlockingTest { + bobSession.cryptoService().getMyCryptoDevice() + } + val txID = testHelper.runBlockingTest { + aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), forceDownload = true) + aliceVerificationService.beginDeviceVerification(bobSession.myUserId, bobDevice.deviceId) + } assertNotNull("Alice should have a started transaction", txID) val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) @@ -90,16 +92,13 @@ class SASTest : InstrumentedTest { val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID) assertNotNull("Bob should have started verif transaction", bobKeyTx) - assertTrue(bobKeyTx is SASDefaultVerificationTransaction) + assertTrue(bobKeyTx is SasVerificationTransaction) assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) - assertTrue(aliceKeyTx is SASDefaultVerificationTransaction) + assertTrue(aliceKeyTx is SasVerificationTransaction) assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) - val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction? - val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction? - - assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state) - assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state) + assertEquals("Alice state should be started", VerificationTxState.OnStarted, aliceKeyTx.state) + assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobKeyTx.state) // Let's cancel from alice side val cancelLatch = CountDownLatch(1) @@ -107,7 +106,7 @@ class SASTest : InstrumentedTest { val bobListener2 = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if (tx.transactionId == txID) { - val immutableState = (tx as SASDefaultVerificationTransaction).state + val immutableState = (tx as SasVerificationTransaction).state if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) { cancelLatch.countDown() } @@ -116,14 +115,16 @@ class SASTest : InstrumentedTest { } bobVerificationService.addListener(bobListener2) - aliceSasTx.cancel(CancelCode.User) + testHelper.runBlockingTest { + aliceKeyTx.cancel(CancelCode.User) + } testHelper.await(cancelLatch) - assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled) - assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled) + assertTrue("Should be cancelled on alice side", aliceKeyTx.state is VerificationTxState.Cancelled) + assertTrue("Should be cancelled on bob side", bobKeyTx.state is VerificationTxState.Cancelled) - val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled - val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled + val aliceCancelState = aliceKeyTx.state as VerificationTxState.Cancelled + val bobCancelState = bobKeyTx.state as VerificationTxState.Cancelled assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe) assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe) @@ -131,9 +132,6 @@ class SASTest : InstrumentedTest { assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode) assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.cancelCode) - assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) - assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) - cryptoTestData.cleanUp(testHelper) } @@ -177,12 +175,16 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + val aliceDevice = testHelper.runBlockingTest { + aliceSession.cryptoService().getMyCryptoDevice().deviceId + } val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { - if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - (tx as IncomingSasVerificationTransaction).performAccept() + if (tx.state is VerificationTxState.OnStarted && tx is SasVerificationTransaction) { + testHelper.runBlockingTest { + tx.acceptVerification() + } } } } @@ -226,7 +228,9 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + val aliceDevice = testHelper.runBlockingTest { + aliceSession.cryptoService().getMyCryptoDevice().deviceId + } fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) @@ -267,7 +271,9 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId + val aliceDevice = testHelper.runBlockingTest { + aliceSession.cryptoService().getMyCryptoDevice().deviceId + } fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) @@ -283,12 +289,15 @@ class SASTest : InstrumentedTest { aliceUserID: String?, aliceDevice: String?, tid: String, - protocols: List = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, - hashes: List = SASDefaultVerificationTransaction.KNOWN_HASHES, - mac: List = SASDefaultVerificationTransaction.KNOWN_MACS, - codes: List = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) { + protocols: List = emptyList(), + hashes: List = emptyList(), + mac: List = emptyList(), + codes: List = emptyList()) { + val deviceId = runBlocking { + bobSession.cryptoService().getMyCryptoDevice().deviceId + } val startMessage = KeyVerificationStart( - fromDevice = bobSession.cryptoService().getMyDevice().deviceId, + fromDevice = deviceId, method = VerificationMethod.SAS.toValue(), transactionId = tid, keyAgreementProtocols = protocols, @@ -323,16 +332,16 @@ class SASTest : InstrumentedTest { val aliceVerificationService = aliceSession.cryptoService().verificationService() val aliceCreatedLatch = CountDownLatch(2) - val aliceCancelledLatch = CountDownLatch(2) - val createdTx = mutableListOf() + val aliceCancelledLatch = CountDownLatch(1) + val createdTx = mutableListOf() val aliceListener = object : VerificationService.Listener { override fun transactionCreated(tx: VerificationTransaction) { - createdTx.add(tx as SASDefaultVerificationTransaction) + createdTx.add(tx) aliceCreatedLatch.countDown() } override fun transactionUpdated(tx: VerificationTransaction) { - if ((tx as SASDefaultVerificationTransaction).state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { + if (tx.state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { aliceCancelledLatch.countDown() } } @@ -340,10 +349,14 @@ class SASTest : InstrumentedTest { aliceVerificationService.addListener(aliceListener) val bobUserId = bobSession!!.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - + val bobDeviceId = testHelper.runBlockingTest { + bobSession.cryptoService().getMyCryptoDevice().deviceId + } + testHelper.runBlockingTest { + aliceSession.cryptoService().downloadKeys(listOf(bobUserId), forceDownload = true) + aliceVerificationService.beginDeviceVerification(bobUserId, bobDeviceId) + aliceVerificationService.beginDeviceVerification(bobUserId, bobDeviceId) + } testHelper.await(aliceCreatedLatch) testHelper.await(aliceCancelledLatch) @@ -366,17 +379,10 @@ class SASTest : InstrumentedTest { val aliceVerificationService = aliceSession.cryptoService().verificationService() val bobVerificationService = bobSession!!.cryptoService().verificationService() - var accepted: ValidVerificationInfoAccept? = null - var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null - val aliceAcceptedLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { - Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}") - if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { - val at = tx as SASDefaultVerificationTransaction - accepted = at.accepted - startReq = at.startReq + if (tx.state is VerificationTxState.OnAccepted) { aliceAcceptedLatch.countDown() } } @@ -385,90 +391,62 @@ class SASTest : InstrumentedTest { val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { - Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}") - if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + if (tx.state is VerificationTxState.OnStarted && tx is SasVerificationTransaction) { bobVerificationService.removeListener(this) - val at = tx as IncomingSasVerificationTransaction - at.performAccept() + testHelper.runBlockingTest { + tx.acceptVerification() + } } } } bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - testHelper.await(aliceAcceptedLatch) - - assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) - - // check that agreement is valid - assertTrue("Agreed Protocol should be Valid", accepted != null) - assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol)) - assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash)) - assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode)) - - accepted!!.shortAuthenticationStrings.forEach { - assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) + val bobDeviceId = runBlocking { + bobSession.cryptoService().getMyCryptoDevice().deviceId } + testHelper.runBlockingTest { + // aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + } + testHelper.await(aliceAcceptedLatch) cryptoTestData.cleanUp(testHelper) } @Test fun test_aliceAndBobSASCode() { + val supportedMethods = listOf(VerificationMethod.SAS) val testHelper = CommonTestHelper(context()) val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - + val sasTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper) val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession + val bobSession = cryptoTestData.secondSession!! + val transactionId = sasTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, supportedMethods) - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val aliceSASLatch = CountDownLatch(1) + val latch = CountDownLatch(2) val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as OutgoingSasVerificationTransaction).uxState - when (uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - aliceSASLatch.countDown() - } - else -> Unit - } + Timber.v("Alice transactionUpdated: ${tx.state}") + latch.countDown() } } - aliceVerificationService.addListener(aliceListener) - - val bobSASLatch = CountDownLatch(1) + aliceSession.cryptoService().verificationService().addListener(aliceListener) val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as IncomingSasVerificationTransaction).uxState - when (uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - tx.performAccept() - } - else -> Unit - } - if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { - bobSASLatch.countDown() - } + Timber.v("Bob transactionUpdated: ${tx.state}") + latch.countDown() } } - bobVerificationService.addListener(bobListener) + bobSession.cryptoService().verificationService().addListener(bobListener) + testHelper.runBlockingTest { + aliceSession.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, transactionId) + } + testHelper.await(latch) + val aliceTx = aliceSession.cryptoService().verificationService().getExistingTransaction(bobSession.myUserId, transactionId) as SasVerificationTransaction + val bobTx = bobSession.cryptoService().verificationService().getExistingTransaction(aliceSession.myUserId, transactionId) as SasVerificationTransaction - val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - testHelper.await(aliceSASLatch) - testHelper.await(bobSASLatch) - - val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction - val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction - - assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), - bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) + assertEquals("Should have same SAS", aliceTx.getDecimalCodeRepresentation(), bobTx.getDecimalCodeRepresentation()) cryptoTestData.cleanUp(testHelper) } @@ -478,7 +456,8 @@ class SASTest : InstrumentedTest { val testHelper = CommonTestHelper(context()) val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - + val sasVerificationTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper) + val transactionId = sasVerificationTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, listOf(VerificationMethod.SAS)) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession @@ -487,21 +466,26 @@ class SASTest : InstrumentedTest { val aliceSASLatch = CountDownLatch(1) val aliceListener = object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + Timber.v("RequestUpdated pr=$pr") + } + var matchOnce = true override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as OutgoingSasVerificationTransaction).uxState - Log.v("TEST", "== aliceState ${uxState.name}") - when (uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + Timber.v("Alice transactionUpdated: ${tx.state}") + if (tx !is SasVerificationTransaction) return + when (tx.state) { + VerificationTxState.ShortCodeReady -> testHelper.runBlockingTest { tx.userHasVerifiedShortCode() } - OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + VerificationTxState.Verified -> { if (matchOnce) { matchOnce = false aliceSASLatch.countDown() } } - else -> Unit + else -> Unit } } } @@ -511,41 +495,53 @@ class SASTest : InstrumentedTest { val bobListener = object : VerificationService.Listener { var acceptOnce = true var matchOnce = true + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + Timber.v("RequestUpdated: pr=$pr") + } + override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as IncomingSasVerificationTransaction).uxState - Log.v("TEST", "== bobState ${uxState.name}") - when (uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + Timber.v("Bob transactionUpdated: ${tx.state}") + if (tx !is SasVerificationTransaction) return + when (tx.state) { + VerificationTxState.OnStarted -> testHelper.runBlockingTest { if (acceptOnce) { acceptOnce = false - tx.performAccept() + tx.acceptVerification() } } - IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + VerificationTxState.ShortCodeReady -> testHelper.runBlockingTest { if (matchOnce) { matchOnce = false tx.userHasVerifiedShortCode() } } - IncomingSasVerificationTransaction.UxState.VERIFIED -> { + VerificationTxState.ShortCodeAccepted -> { bobSASLatch.countDown() } - else -> Unit + else -> Unit } } } bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) + val bobDeviceId = runBlocking { + bobSession.cryptoService().getMyCryptoDevice().deviceId + } + testHelper.runBlockingTest { + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, transactionId) + } testHelper.await(aliceSASLatch) testHelper.await(bobSASLatch) // Assert that devices are verified - val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) - + val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = testHelper.runBlockingTest { + aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId) + } + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = testHelper.runBlockingTest { + bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyCryptoDevice().deviceId) + } assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) cryptoTestData.cleanUp(testHelper) @@ -563,11 +559,13 @@ class SASTest : InstrumentedTest { val aliceVerificationService = aliceSession.cryptoService().verificationService() val bobVerificationService = bobSession!!.cryptoService().verificationService() - val req = aliceVerificationService.requestKeyVerificationInDMs( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - bobSession.myUserId, - cryptoTestData.roomId - ) + val req = testHelper.runBlockingTest { + aliceVerificationService.requestKeyVerificationInDMs( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + bobSession.myUserId, + cryptoTestData.roomId + ) + } var requestID: String? = null @@ -590,11 +588,13 @@ class SASTest : InstrumentedTest { } } - bobVerificationService.readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceSession.myUserId, - requestID!! - ) + testHelper.runBlockingTest { + bobVerificationService.readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + requestID!! + ) + } // wait for alice to get the ready testHelper.waitWithLatch { @@ -606,19 +606,19 @@ class SASTest : InstrumentedTest { } // Start concurrent! - aliceVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID!!, - cryptoTestData.roomId, - bobSession.myUserId, - bobSession.sessionParams.deviceId!!) + testHelper.runBlockingTest { + aliceVerificationService.requestKeyVerificationInDMs( + methods = listOf(VerificationMethod.SAS), + otherUserId = bobSession.myUserId, + roomId = cryptoTestData.roomId + ) - bobVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID!!, - cryptoTestData.roomId, - aliceSession.myUserId, - aliceSession.sessionParams.deviceId!!) + bobVerificationService.requestKeyVerificationInDMs( + methods = listOf(VerificationMethod.SAS), + otherUserId = aliceSession.myUserId, + roomId = cryptoTestData.roomId + ) + } // we should reach SHOW SAS on both var alicePovTx: SasVerificationTransaction? diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt new file mode 100644 index 0000000000..52075c9497 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt @@ -0,0 +1,86 @@ +/* + * 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.verification + +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestData +import org.matrix.android.sdk.common.CryptoTestHelper +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +class SasVerificationTestHelper(private val testHelper: CommonTestHelper, private val cryptoTestHelper: CryptoTestHelper) { + fun requestVerificationAndWaitForReadyState(cryptoTestData: CryptoTestData, supportedMethods: List): String { + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + cryptoTestHelper.initializeCrossSigning(aliceSession) + cryptoTestHelper.initializeCrossSigning(bobSession) + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() + + var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null + + val latch = CountDownLatch(2) + val aliceListener = object : VerificationService.Listener { + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 4: Alice receive the ready request + Timber.v("Alice request updated: $pr") + if (pr.isReady) { + latch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // Step 2: Bob accepts the verification request + Timber.v("Bob accepts the verification request") + testHelper.runBlockingTest { + bobVerificationService.readyPendingVerification( + supportedMethods, + aliceSession.myUserId, + pr.transactionId!! + ) + } + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 3: Bob is ready + Timber.v("Bob request updated $pr") + if (pr.isReady) { + bobReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + // Step 1: Alice starts a verification request + testHelper.runBlockingTest { + aliceVerificationService.requestKeyVerificationInDMs(supportedMethods, bobUserId, cryptoTestData.roomId) + } + testHelper.await(latch) + bobVerificationService.removeListener(bobListener) + aliceVerificationService.removeListener(aliceListener) + return bobReadyPendingVerificationRequest?.transactionId!! + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt similarity index 71% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt index 35c5a4dab9..f32250d055 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto.verification.qrcode +package org.matrix.android.sdk.internal.crypto.verification import androidx.test.ext.junit.runners.AndroidJUnit4 import org.amshove.kluent.shouldBe @@ -23,19 +23,13 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.TestConstants +import timber.log.Timber import java.util.concurrent.CountDownLatch -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -147,50 +141,19 @@ class VerificationTest : InstrumentedTest { ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true) ) - // TODO Add tests without SAS - private fun doTest(aliceSupportedMethods: List, bobSupportedMethods: List, expectedResultForAlice: ExpectedResult, expectedResultForBob: ExpectedResult) { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! - testHelper.doSync { callback -> - aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, callback) - } - - testHelper.doSync { callback -> - bobSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, callback) - } + cryptoTestHelper.initializeCrossSigning(aliceSession) + cryptoTestHelper.initializeCrossSigning(bobSession) val aliceVerificationService = aliceSession.cryptoService().verificationService() val bobVerificationService = bobSession.cryptoService().verificationService() @@ -202,6 +165,7 @@ class VerificationTest : InstrumentedTest { val aliceListener = object : VerificationService.Listener { override fun verificationRequestUpdated(pr: PendingVerificationRequest) { // Step 4: Alice receive the ready request + Timber.v("Alice is ready: ${pr.isReady}") if (pr.isReady) { aliceReadyPendingVerificationRequest = pr latch.countDown() @@ -213,16 +177,19 @@ class VerificationTest : InstrumentedTest { val bobListener = object : VerificationService.Listener { override fun verificationRequestCreated(pr: PendingVerificationRequest) { // Step 2: Bob accepts the verification request - bobVerificationService.readyPendingVerificationInDMs( - bobSupportedMethods, - aliceSession.myUserId, - cryptoTestData.roomId, - pr.transactionId!! - ) + Timber.v("Bob accepts the verification request") + testHelper.runBlockingTest { + bobVerificationService.readyPendingVerification( + bobSupportedMethods, + aliceSession.myUserId, + pr.transactionId!! + ) + } } override fun verificationRequestUpdated(pr: PendingVerificationRequest) { // Step 3: Bob is ready + Timber.v("Bob is ready: ${pr.isReady}") if (pr.isReady) { bobReadyPendingVerificationRequest = pr latch.countDown() @@ -233,7 +200,9 @@ class VerificationTest : InstrumentedTest { val bobUserId = bobSession.myUserId // Step 1: Alice starts a verification request - aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) + testHelper.runBlockingTest { + aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) + } testHelper.await(latch) aliceReadyPendingVerificationRequest!!.let { pr -> diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt deleted file mode 100644 index 9b10f9e9af..0000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 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.qrcode - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldNotBeEqualTo -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class SharedSecretTest : InstrumentedTest { - - @Test - fun testSharedSecretLengthCase() { - repeat(100) { - generateSharedSecretV2().length shouldBe 11 - } - } - - @Test - fun testSharedDiffCase() { - val sharedSecret1 = generateSharedSecretV2() - val sharedSecret2 = generateSharedSecretV2() - - sharedSecret1 shouldNotBeEqualTo sharedSecret2 - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 699dbf1d92..4a7b698f32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context import androidx.lifecycle.LiveData import androidx.paging.PagedList -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -32,14 +32,12 @@ import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.NewSessionListener import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest -import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse interface CryptoService { @@ -49,9 +47,9 @@ interface CryptoService { fun keysBackupService(): KeysBackupService - fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) + suspend fun setDeviceName(deviceId: String, deviceName: String) - fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) fun getCryptoVersion(context: Context, longFormat: Boolean): String @@ -61,11 +59,9 @@ interface CryptoService { fun setWarnOnUnknownDevices(warn: Boolean) - fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) + suspend fun getUserDevices(userId: String): MutableList - fun getUserDevices(userId: String): MutableList - - fun getMyDevice(): CryptoDeviceInfo + suspend fun getMyCryptoDevice(): CryptoDeviceInfo fun getGlobalBlacklistUnverifiedDevices(): Boolean @@ -73,8 +69,6 @@ interface CryptoService { fun setRoomUnBlacklistUnverifiedDevices(roomId: String) - fun getDeviceTrackingStatus(userId: String): Int - suspend fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?): ImportRoomKeysResult @@ -83,7 +77,7 @@ interface CryptoService { fun setRoomBlacklistUnverifiedDevices(roomId: String) - fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? fun reRequestRoomKeyForEvent(event: Event) @@ -91,29 +85,26 @@ interface CryptoService { fun removeRoomKeysRequestListener(listener: GossipingRequestListener) - fun fetchDevicesList(callback: MatrixCallback) + suspend fun fetchDevicesList(): List fun getMyDevicesInfo(): List fun getLiveMyDevicesInfo(): LiveData> - fun getDeviceInfo(deviceId: String, callback: MatrixCallback) + suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo - fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun isRoomEncrypted(roomId: String): Boolean - fun encryptEventContent(eventContent: Content, + suspend fun encryptEventContent(eventContent: Content, eventType: String, - roomId: String, - callback: MatrixCallback) + roomId: String): MXEncryptEventContentResult fun discardOutboundSession(roomId: String) @Throws(MXCryptoError::class) - fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult - - fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) + suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult fun getEncryptionAlgorithm(roomId: String): String? @@ -121,11 +112,11 @@ interface CryptoService { suspend fun downloadKeys(userIds: List, forceDownload: Boolean = false): MXUsersDevicesMap - fun getCryptoDeviceInfo(userId: String): List + suspend fun getCryptoDeviceInfoList(userId: String): List - fun getLiveCryptoDeviceInfo(userId: String): LiveData> + fun getLiveCryptoDeviceInfoList(userId: String): Flow> - fun getLiveCryptoDeviceInfo(userIds: List): LiveData> + fun getLiveCryptoDeviceInfoList(userIds: List): Flow> fun addNewSessionListener(newSessionListener: NewSessionListener) @@ -150,7 +141,7 @@ interface CryptoService { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. */ - fun prepareToEncrypt(roomId: String, callback: MatrixCallback) + suspend fun prepareToEncrypt(roomId: String) /** * When LL all room members might not be loaded when setting up encryption. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 1d97488ebc..edc9c3b2dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -16,8 +16,7 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning -import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.util.Optional @@ -29,31 +28,30 @@ interface CrossSigningService { /** * Is our own device signed by our own cross signing identity */ - fun isCrossSigningVerified(): Boolean + suspend fun isCrossSigningVerified(): Boolean // TODO this isn't used anywhere besides in tests? // Is this the local trust concept that we have for devices? - fun isUserTrusted(otherUserId: String): Boolean + suspend fun isUserTrusted(otherUserId: String): Boolean /** * Will not force a download of the key, but will verify signatures trust chain. * Checks that my trusted user key has signed the other user UserKey */ - fun checkUserTrust(otherUserId: String): UserTrustResult + suspend fun checkUserTrust(otherUserId: String): UserTrustResult /** * Initialize cross signing for this user. * Users needs to enter credentials */ - fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, - callback: MatrixCallback) + suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) /** * Does our own user have a valid cross signing identity uploaded. * * In other words has any of our devices uploaded public cross signing keys to the server. */ - fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null + suspend fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null /** * Inject the private cross signing keys, likely from backup, into our store. @@ -62,25 +60,25 @@ interface CrossSigningService { * by the server and if they do so */ suspend fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, - uskKeyPrivateKey: String?, - sskPrivateKey: String?): UserTrustResult + uskKeyPrivateKey: String?, + sskPrivateKey: String?): UserTrustResult /** * Get the public cross signing keys for the given user * * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. */ - fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? - fun getLiveCrossSigningKeys(userId: String): LiveData> + fun getLiveCrossSigningKeys(userId: String): Flow> /** Get our own public cross signing keys */ - fun getMyCrossSigningKeys(): MXCrossSigningInfo? + suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? /** Get our own private cross signing keys */ - fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? - fun getLiveCrossSigningPrivateKeys(): LiveData> + fun getLiveCrossSigningPrivateKeys(): Flow> /** * Can we sign our other devices or other users? @@ -93,11 +91,10 @@ interface CrossSigningService { fun allPrivateKeysKnown(): Boolean /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server */ - fun trustUser(otherUserId: String, - callback: MatrixCallback) + suspend fun trustUser(otherUserId: String) /** Mark our own master key as trusted */ - fun markMyMasterKeyAsTrusted() + suspend fun markMyMasterKeyAsTrusted() /** * Sign one of your devices and upload the signature @@ -114,10 +111,10 @@ interface CrossSigningService { * using the self-signing key for our own devices or using the user-signing key and the master * key of another user. */ - fun checkDeviceTrust(otherUserId: String, - otherDeviceId: String, - // TODO what is locallyTrusted used for? - locallyTrusted: Boolean?): DeviceTrustResult + suspend fun checkDeviceTrust(otherUserId: String, + otherDeviceId: String, + // TODO what is locallyTrusted used for? + locallyTrusted: Boolean?): DeviceTrustResult // FIXME Those method do not have to be in the service // TODO those three methods doesn't seem to be used anywhere? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt index 6bb4dbc620..5a0ce933de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo interface KeysBackupService { + /** * Retrieve the current version of the backup from the homeserver * @@ -45,12 +46,12 @@ interface KeysBackupService { /** * Facility method to get the total number of locally stored keys */ - fun getTotalNumbersOfKeys(): Int + suspend fun getTotalNumbersOfKeys(): Int /** * Facility method to get the number of backed up keys */ - fun getTotalNumbersOfBackedUpKeys(): Int + suspend fun getTotalNumbersOfBackedUpKeys(): Int // /** // * Start to back up keys immediately. @@ -71,7 +72,7 @@ interface KeysBackupService { /** * Return the current progress of the backup */ - fun getBackupProgress(progressListener: ProgressListener) + suspend fun getBackupProgress(progressListener: ProgressListener) /** * Get information about a backup version defined on the homeserver. @@ -128,7 +129,7 @@ interface KeysBackupService { * Ask if the backup on the server contains keys that we may do not have locally. * This should be called when entering in the state READY_TO_BACKUP */ - fun canRestoreKeys(): Boolean + suspend fun canRestoreKeys(): Boolean /** * Set trust on a keys backup version. @@ -199,7 +200,7 @@ interface KeysBackupService { // For gossiping fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) - fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? + suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt deleted file mode 100644 index db2ea72e88..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 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.api.session.crypto.verification - -interface IncomingSasVerificationTransaction : SasVerificationTransaction { - val uxState: UxState - - fun performAccept() - - enum class UxState { - UNKNOWN, - SHOW_ACCEPT, - WAIT_FOR_KEY_AGREEMENT, - SHOW_SAS, - WAIT_FOR_VERIFICATION, - VERIFIED, - CANCELLED_BY_ME, - CANCELLED_BY_OTHER - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt deleted file mode 100644 index 38ee5dc7e7..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 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.api.session.crypto.verification - -interface OutgoingSasVerificationTransaction : SasVerificationTransaction { - val uxState: UxState - - enum class UxState { - UNKNOWN, - WAIT_FOR_START, - WAIT_FOR_KEY_AGREEMENT, - SHOW_SAS, - WAIT_FOR_VERIFICATION, - VERIFIED, - CANCELLED_BY_ME, - CANCELLED_BY_OTHER - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt index 37855099be..3e3dac633f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt @@ -26,15 +26,15 @@ interface QrCodeVerificationTransaction : VerificationTransaction { /** * Call when you have scan the other user QR code */ - fun userHasScannedOtherQrCode(otherQrCodeText: String) + suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) /** * Call when you confirm that other user has scanned your QR code */ - fun otherUserScannedMyQrCode() + suspend fun otherUserScannedMyQrCode() /** * Call when you do not confirm that other user has scanned your QR code */ - fun otherUserDidNotScannedMyQrCode() + suspend fun otherUserDidNotScannedMyQrCode() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt index 2922d98a62..61f33e25ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt @@ -20,8 +20,6 @@ interface SasVerificationTransaction : VerificationTransaction { fun supportsEmoji(): Boolean - fun supportsDecimal(): Boolean - fun getEmojiCodeRepresentation(): List fun getDecimalCodeRepresentation(): String @@ -30,9 +28,9 @@ interface SasVerificationTransaction : VerificationTransaction { * To be called by the client when the user has verified that * both short codes do match */ - fun userHasVerifiedShortCode() + suspend fun userHasVerifiedShortCode() - fun acceptVerification() + suspend fun acceptVerification() - fun shortCodeDoesNotMatch() + suspend fun shortCodeDoesNotMatch() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index b9d0c0ad2c..bca6c31897 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.crypto.verification -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.LocalEcho /** @@ -36,7 +35,7 @@ interface VerificationService { /** * Mark this device as verified manually */ - fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? @@ -46,54 +45,37 @@ interface VerificationService { fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? - fun beginKeyVerification(method: VerificationMethod, - otherUserId: String, - otherDeviceId: String, - transactionId: String?): String? + /** + * Request key verification with another user via room events (instead of the to-device API). + */ + suspend fun requestKeyVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest /** - * Request key verification with another user via room events (instead of the to-device API) + * Request a self key verification using to-device API (instead of room events). */ - fun requestKeyVerificationInDMs(methods: List, - otherUserId: String, - roomId: String, - localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest - - fun cancelVerificationRequest(request: PendingVerificationRequest) + suspend fun requestSelfKeyVerification(methods: List): PendingVerificationRequest /** - * Request a key verification from another user using toDevice events. + * You should call this method after receiving a verification request. + * Accept the verification request advertising the given methods as supported + * Returns false if the request is unknown or transaction is not ready. */ - fun requestKeyVerification(methods: List, - otherUserId: String, - otherDevices: List?): PendingVerificationRequest + suspend fun readyPendingVerification(methods: List, + otherUserId: String, + transactionId: String): Boolean - fun declineVerificationRequestInDMs(otherUserId: String, - transactionId: String, - roomId: String) + suspend fun cancelVerificationRequest(request: PendingVerificationRequest) - // Only SAS method is supported for the moment - // TODO Parameter otherDeviceId should be removed in this case - fun beginKeyVerificationInDMs(method: VerificationMethod, - transactionId: String, - roomId: String, - otherUserId: String, - otherDeviceId: String): String + suspend fun cancelVerificationRequest(otherUserId: String, transactionId: String) - /** - * Returns false if the request is unknown - */ - fun readyPendingVerificationInDMs(methods: List, - otherUserId: String, - roomId: String, - transactionId: String): Boolean + suspend fun beginKeyVerification(method: VerificationMethod, + otherUserId: String, + transactionId: String): String? - /** - * Returns false if the request is unknown - */ - fun readyPendingVerification(methods: List, - otherUserId: String, - transactionId: String): Boolean + suspend fun beginDeviceVerification(otherUserId: String, otherDeviceId: String): String? interface Listener { /** @@ -137,6 +119,4 @@ interface VerificationService { return age in tooInThePast..tooInTheFuture } } - - fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt index 4d35bc44ac..389309d915 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt @@ -30,9 +30,9 @@ interface VerificationTransaction { /** * User wants to cancel the transaction */ - fun cancel() + suspend fun cancel() - fun cancel(code: CancelCode) + suspend fun cancel(code: CancelCode) fun isToDeviceTransport(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 0c77b574e7..d33e318cc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -91,6 +91,7 @@ object EventType { const val SEND_SECRET = "m.secret.send" // Interactive key verification + const val KEY_VERIFICATION_REQUEST = "m.key.verification.request" const val KEY_VERIFICATION_START = "m.key.verification.start" const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" const val KEY_VERIFICATION_KEY = "m.key.verification.key" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index 2a6138ae60..f4eb4b21a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.room.model.message +import org.matrix.android.sdk.api.session.events.model.EventType + object MessageType { const val MSGTYPE_TEXT = "m.text" const val MSGTYPE_EMOTE = "m.emote" @@ -26,7 +28,7 @@ object MessageType { const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" - const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" + const val MSGTYPE_VERIFICATION_REQUEST = EventType.KEY_VERIFICATION_REQUEST // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt new file mode 100644 index 0000000000..64b513552b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt @@ -0,0 +1,44 @@ +/* + * 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.coroutines.builder + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.channels.ProducerScope + +/** + * Use this with a flow builder like [kotlinx.coroutines.flow.channelFlow] to replace [kotlinx.coroutines.channels.awaitClose]. + * As awaitClose is at the end of the builder block, it can lead to the block being cancelled before it reaches the awaitClose. + * Example of usage: + * + * return channelFlow { + * val onClose = safeInvokeOnClose { + * // Do stuff on close + * } + * val data = getData() + * send(data) + * onClose.await() + * } + * + */ +internal fun ProducerScope.safeInvokeOnClose(handler: (cause: Throwable?) -> Unit): CompletableDeferred { + val onClose = CompletableDeferred() + invokeOnClose { + handler(it) + onClose.complete(Unit) + } + return onClose +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 98f65dc34b..f4e0f818eb 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -22,18 +22,17 @@ import androidx.lifecycle.LiveData import androidx.paging.PagedList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull @@ -55,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse -import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult @@ -65,8 +63,9 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent +import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask @@ -76,14 +75,9 @@ import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.verification.RustVerificationService import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.TaskThread -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.task.launchToCallback import timber.log.Timber import uniffi.olm.Request import uniffi.olm.RequestType @@ -130,9 +124,8 @@ internal class DefaultCryptoService @Inject constructor( private val loadRoomMembersTask: LoadRoomMembersTask, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, - private val sender: RequestSender, + private val requestSender: RequestSender, private val crossSigningService: CrossSigningService, private val verificationService: RustVerificationService, private val keysBackupService: RustKeyBackupService, @@ -153,7 +146,12 @@ internal class DefaultCryptoService @Inject constructor( // Locks for some of our operations private val keyClaimLock: Mutex = Mutex() - private val outgoingRequestsLock: Mutex = Mutex() + private val outgoingRequestsProcessor = OutgoingRequestsProcessor( + requestSender = requestSender, + coroutineScope = cryptoCoroutineScope, + cryptoSessionInfoProvider = cryptoSessionInfoProvider, + shieldComputer = crossSigningService::shieldForGroup + ) private val roomKeyShareLocks: ConcurrentHashMap = ConcurrentHashMap() fun onStateEvent(roomId: String, event: Event) { @@ -166,51 +164,33 @@ internal class DefaultCryptoService @Inject constructor( fun onLiveEvent(roomId: String, event: Event) { if (event.isStateEvent()) { - when (event.getClearType()) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) - else -> cryptoCoroutineScope.launch { - this@DefaultCryptoService.verificationService.onEvent(event) + when (event.getClearType()) { + EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } + } else { + cryptoCoroutineScope.launch { + verificationService.onEvent(roomId, event) } - } } } private val gossipingBuffer = mutableListOf() - override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { - setDeviceNameTask - .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { - this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - // bg refresh of crypto device - cryptoCoroutineScope.launch { - try { - downloadKeys(listOf(userId), true) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device") - } - } - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) + override suspend fun setDeviceName(deviceId: String, deviceName: String) { + val params = SetDeviceNameTask.Params(deviceId, deviceName) + setDeviceNameTask.execute(params) + try { + downloadKeys(listOf(userId), true) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device") + } } - override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { - deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { + val params = DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null) + deleteDeviceTask.execute(params) } override fun getCryptoVersion(context: Context, longFormat: Boolean): String { @@ -218,27 +198,16 @@ internal class DefaultCryptoService @Inject constructor( return if (longFormat) "Rust SDK 0.3" else "0.3" } - override fun getMyDevice(): CryptoDeviceInfo { - return runBlocking { olmMachine.ownDevice() } + override suspend fun getMyCryptoDevice(): CryptoDeviceInfo { + return olmMachine.ownDevice() } - override fun fetchDevicesList(callback: MatrixCallback) { - getDevicesTask - .configureWith { - // this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - - override fun onSuccess(data: DevicesListResponse) { - // Save in local DB - cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) - callback.onSuccess(data) - } - } - } - .executeBy(taskExecutor) + override suspend fun fetchDevicesList(): List { + val devicesList = tryOrNull { + getDevicesTask.execute(Unit).devices + }.orEmpty() + cryptoStore.saveMyDevicesInfo(devicesList) + return devicesList } override fun getLiveMyDevicesInfo(): LiveData> { @@ -249,16 +218,12 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getMyDevicesInfo() } - override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { - getDeviceInfoTask - .configureWith(GetDeviceInfoTask.Params(deviceId)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo { + val params = GetDeviceInfoTask.Params(deviceId) + return getDeviceInfoTask.execute(params) } - override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + override suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { return if (onlyBackedUp) { keysBackupService.getTotalNumbersOfBackedUpKeys() } else { @@ -267,20 +232,6 @@ internal class DefaultCryptoService @Inject constructor( // return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) } - /** - * Provides the tracking status - * - * @param userId the user id - * @return the tracking status - */ - override fun getDeviceTrackingStatus(userId: String): Int { - return if (olmMachine.isUserTracked(userId)) { - 3 - } else { - -1 - } - } - /** * Tell if the MXCrypto is started * @@ -299,29 +250,13 @@ internal class DefaultCryptoService @Inject constructor( */ fun start() { internalStart() - // Just update - fetchDevicesList(NoOpMatrixCallback()) - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch { + // Just update + fetchDevicesList() cryptoStore.tidyUpDataBase() } } - fun ensureDevice() { - cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) { - // Open the store - cryptoStore.open() - - // this can throw if no backup - /* - TODO - tryOrNull { - keysBackupService.checkAndStartKeysBackup() - } - */ - } - } - private fun internalStart() { if (isStarted.get() || isStarting.get()) { return @@ -355,9 +290,13 @@ internal class DefaultCryptoService @Inject constructor( /** * Close the crypto */ - fun close() = runBlocking(coroutineDispatchers.crypto) { + fun close() { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - cryptoStore.close() + cryptoCoroutineScope.launch { + withContext(coroutineDispatchers.crypto + NonCancellable) { + cryptoStore.close() + } + } } // Always enabled on Matrix Android SDK2 @@ -380,7 +319,7 @@ internal class DefaultCryptoService @Inject constructor( */ suspend fun onSyncCompleted() { if (isStarted()) { - sendOutgoingRequests() + outgoingRequestsProcessor.process(olmMachine) // This isn't a copy paste error. Sending the outgoing requests may // claim one-time keys and establish 1-to-1 Olm sessions with devices, while some // outgoing requests are waiting for an Olm session to be established (e.g. forwarding @@ -389,7 +328,7 @@ internal class DefaultCryptoService @Inject constructor( // The second call sends out those requests that are waiting for the // keys claim request to be sent out. // This could be omitted but then devices might be waiting for the next - sendOutgoingRequests() + outgoingRequestsProcessor.process(olmMachine) keysBackupService.maybeBackupKeys() } @@ -410,41 +349,19 @@ internal class DefaultCryptoService @Inject constructor( * @param userId the user id * @param deviceId the device id */ - override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { - return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { - runBlocking { - this@DefaultCryptoService.olmMachine.getCryptoDeviceInfo(userId, deviceId) - } - } else { - null - } + override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { + if (userId.isEmpty() || deviceId.isNullOrEmpty()) return null + return olmMachine.getCryptoDeviceInfo(userId, deviceId) } - override fun getCryptoDeviceInfo(userId: String): List { - return runBlocking { - this@DefaultCryptoService.olmMachine.getCryptoDeviceInfo(userId) - } + override suspend fun getCryptoDeviceInfoList(userId: String): List { + return olmMachine.getCryptoDeviceInfo(userId) } - override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { - return getLiveCryptoDeviceInfo(listOf(userId)) - } + override fun getLiveCryptoDeviceInfoList(userId: String) = getLiveCryptoDeviceInfoList(listOf(userId)) - override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { - return runBlocking { - this@DefaultCryptoService.olmMachine.getLiveDevices(userIds) // ?: LiveDevice(userIds, deviceObserver) - } - } - - /** - * Update the blocked/verified state of the given device. - * - * @param trustLevel the new trust level - * @param userId the owner of the device - * @param deviceId the unique identifier for the device. - */ - override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { - // TODO + override fun getLiveCryptoDeviceInfoList(userIds: List): Flow> { + return olmMachine.getLiveDevices(userIds) } /** @@ -503,8 +420,8 @@ internal class DefaultCryptoService @Inject constructor( /** * @return the stored device keys for a user. */ - override fun getUserDevices(userId: String): MutableList { - return this.getCryptoDeviceInfo(userId).toMutableList() + override suspend fun getUserDevices(userId: String): MutableList { + return this.getCryptoDeviceInfoList(userId).toMutableList() } private fun isEncryptionEnabledForInvitedUser(): Boolean { @@ -533,34 +450,29 @@ internal class DefaultCryptoService @Inject constructor( * @param eventContent the content of the event. * @param eventType the type of the event. * @param roomId the room identifier the event will be sent. - * @param callback the asynchronous callback */ - override fun encryptEventContent(eventContent: Content, - eventType: String, - roomId: String, - callback: MatrixCallback) { + override suspend fun encryptEventContent(eventContent: Content, + eventType: String, + roomId: String): MXEncryptEventContentResult { // moved to crypto scope to have up to date values - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + return withContext(coroutineDispatchers.crypto) { val algorithm = getEncryptionAlgorithm(roomId) - if (algorithm != null) { val userIds = getRoomUserIds(roomId) val t0 = System.currentTimeMillis() Timber.tag(loggerTag.value).v("encryptEventContent() starts") - runCatching { - measureTimeMillis { - preshareRoomKey(roomId, userIds) - }.also { - Timber.d("Shared room key in room $roomId took $it ms") - } - val content = encrypt(roomId, eventType, eventContent) - Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") - MXEncryptEventContentResult(content, EventType.ENCRYPTED) - }.foldToCallback(callback) + measureTimeMillis { + preshareRoomKey(roomId, userIds) + }.also { + Timber.d("Shared room key in room $roomId took $it ms") + } + val content = encrypt(roomId, eventType, eventContent) + Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + MXEncryptEventContentResult(content, EventType.ENCRYPTED) } else { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason") - callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) + throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)) } } } @@ -577,22 +489,8 @@ internal class DefaultCryptoService @Inject constructor( * @return the MXEventDecryptionResult data, or throw in case of error */ @Throws(MXCryptoError::class) - override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return runBlocking { - olmMachine.decryptRoomEvent(event) - } - } - - /** - * Decrypt an event asynchronously - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param callback the callback to return data or null - */ - override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - // This isn't really used anywhere, maybe just remove it? - // TODO + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + return olmMachine.decryptRoomEvent(event) } /** @@ -616,7 +514,6 @@ internal class DefaultCryptoService @Inject constructor( // Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") // } finally { val userIds = getRoomUserIds(roomId) - olmMachine.updateTrackedUsers(userIds) setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), userIds) // } } @@ -721,7 +618,7 @@ internal class DefaultCryptoService @Inject constructor( this.keysBackupService.onSecretKeyGossip(secretContent.secretValue) } else -> { - this.verificationService.onEvent(event) + this.verificationService.onEvent(null, event) } } liveEventManager.get().dispatchOnLiveToDevice(event) @@ -730,26 +627,12 @@ internal class DefaultCryptoService @Inject constructor( } private suspend fun preshareRoomKey(roomId: String, roomMembers: List) { - keyClaimLock.withLock { - val request = this.olmMachine.getMissingSessions(roomMembers) - // This request can only be a keys claim request. - if (request != null) { - when (request) { - is Request.KeysClaim -> { - claimKeys(request) - } - else -> { - } - } - } - } - - val keyShareLock = roomKeyShareLocks.getOrPut(roomId, { Mutex() }) + claimMissingKeys(roomMembers) + val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() } var sharedKey = false - keyShareLock.withLock { coroutineScope { - this@DefaultCryptoService.olmMachine.shareRoomKey(roomId, roomMembers).map { + olmMachine.shareRoomKey(roomId, roomMembers).map { when (it) { is Request.ToDevice -> { sharedKey = true @@ -775,19 +658,35 @@ internal class DefaultCryptoService @Inject constructor( } } + private suspend fun claimMissingKeys(roomMembers: List) = keyClaimLock.withLock { + val request = this.olmMachine.getMissingSessions(roomMembers) + // This request can only be a keys claim request. + when (request) { + is Request.KeysClaim -> { + claimKeys(request) + } + else -> { + } + } + } + private suspend fun encrypt(roomId: String, eventType: String, content: Content): Content { return olmMachine.encrypt(roomId, eventType, content) } private suspend fun uploadKeys(request: Request.KeysUpload) { - val response = this.sender.uploadKeys(request) - this.olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_UPLOAD, response) + try { + val response = requestSender.uploadKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_UPLOAD, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO uploadKeys(): error") + } } private suspend fun queryKeys(request: Request.KeysQuery) { try { - val response = this.sender.queryKeys(request) - this.olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response) + val response = requestSender.queryKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response) // Update the shields! cryptoCoroutineScope.launch { @@ -798,69 +697,44 @@ internal class DefaultCryptoService @Inject constructor( } } } catch (throwable: Throwable) { - Timber.tag(loggerTag.value).e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO doKeyDownloadForUsers(): error") } } private suspend fun sendToDevice(request: Request.ToDevice) { - this.sender.sendToDevice(request) - olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}") + try { + requestSender.sendToDevice(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}") + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO sendToDevice(): error") + } } private suspend fun claimKeys(request: Request.KeysClaim) { - val response = this.sender.claimKeys(request) - olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response) + try { + val response = requestSender.claimKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO claimKeys(): error") + } } private suspend fun signatureUpload(request: Request.SignatureUpload) { - this.sender.sendSignatureUpload(request) - olmMachine.markRequestAsSent(request.requestId, RequestType.SIGNATURE_UPLOAD, "{}") + try { + val response = requestSender.sendSignatureUpload(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.SIGNATURE_UPLOAD, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO signatureUpload(): error") + } } - private suspend fun sendOutgoingRequests() { - outgoingRequestsLock.withLock { - coroutineScope { - olmMachine.outgoingRequests().map { - when (it) { - is Request.KeysUpload -> { - async { - uploadKeys(it) - } - } - is Request.KeysQuery -> { - async { - queryKeys(it) - } - } - is Request.ToDevice -> { - async { - sendToDevice(it) - } - } - is Request.KeysClaim -> { - async { - claimKeys(it) - } - } - is Request.RoomMessage -> { - async { - sender.sendRoomMessage(it) - } - } - is Request.SignatureUpload -> { - async { - signatureUpload(it) - } - } - is Request.KeysBackup -> { - async { - // The rust-sdk won't ever produce KeysBackup requests here, - // those only get explicitly created. - } - } - } - }.joinAll() - } + private suspend fun sendRoomMessage(request: Request.RoomMessage) { + try { + Timber.v("SendRoomMessage: $request") + val response = requestSender.sendRoomMessage(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.ROOM_MESSAGE, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO sendRoomMessage(): error") } } @@ -991,18 +865,17 @@ internal class DefaultCryptoService @Inject constructor( val cancellation = requestPair.cancellation val request = requestPair.keyRequest - if (cancellation != null) { - when (cancellation) { - is Request.ToDevice -> { - sendToDevice(cancellation) - } + when (cancellation) { + is Request.ToDevice -> { + sendToDevice(cancellation) } + else -> Unit } - when (request) { is Request.ToDevice -> { sendToDevice(request) } + else -> Unit } } } @@ -1082,18 +955,16 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.logDbUsageInfo() } - override fun prepareToEncrypt(roomId: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + override suspend fun prepareToEncrypt(roomId: String) { + withContext(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") // Ensure to load all room members try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") - callback.onFailure(failure) - return@launch + throw failure } - val userIds = getRoomUserIds(roomId) val algorithm = getEncryptionAlgorithm(roomId) @@ -1101,19 +972,13 @@ internal class DefaultCryptoService @Inject constructor( if (algorithm == null) { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") - callback.onFailure(IllegalArgumentException("Missing algorithm")) - return@launch + throw IllegalArgumentException("Missing algorithm") } - - runCatching { + try { preshareRoomKey(roomId, userIds) - }.fold( - { callback.onSuccess(Unit) }, - { - Timber.tag(loggerTag.value).e(it, "prepareToEncrypt() failed.") - callback.onFailure(it) - } - ) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to PreshareRoomKey") + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt index 9ef3cb200a..8f3491bf0a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt @@ -16,13 +16,14 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.prepareMethods import uniffi.olm.CryptoStoreException import uniffi.olm.OlmMachine @@ -39,16 +40,17 @@ internal class Device( private val machine: OlmMachine, private var inner: InnerDevice, private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, private val listeners: ArrayList ) { @Throws(CryptoStoreException::class) private suspend fun refreshData() { - val device = withContext(Dispatchers.IO) { + val device = withContext(coroutineDispatchers.io) { machine.getDevice(inner.userId, inner.deviceId) } if (device != null) { - this.inner = device + inner = device } } @@ -66,12 +68,12 @@ internal class Device( @Throws(CryptoStoreException::class) suspend fun requestVerification(methods: List): VerificationRequest? { val stringMethods = prepareMethods(methods) - val result = withContext(Dispatchers.IO) { + val result = withContext(coroutineDispatchers.io) { machine.requestVerificationWithDevice(inner.userId, inner.deviceId, stringMethods) } return if (result != null) { - this.sender.sendVerificationRequest(result.request) + sender.sendVerificationRequest(result.request) result.verification } else { null @@ -89,14 +91,18 @@ internal class Device( */ @Throws(CryptoStoreException::class) suspend fun startVerification(): SasVerification? { - val result = withContext(Dispatchers.IO) { + val result = withContext(coroutineDispatchers.io) { machine.startSasWithDevice(inner.userId, inner.deviceId) } return if (result != null) { - this.sender.sendVerificationRequest(result.request) + sender.sendVerificationRequest(result.request) SasVerification( - this.machine, result.sas, this.sender, this.listeners, + machine = machine, + inner = result.sas, + sender = sender, + coroutineDispatchers = coroutineDispatchers, + listeners = listeners ) } else { null @@ -111,7 +117,7 @@ internal class Device( */ @Throws(CryptoStoreException::class) suspend fun markAsTrusted() { - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { machine.markDeviceAsTrusted(inner.userId, inner.deviceId) } } @@ -127,11 +133,11 @@ internal class Device( */ @Throws(SignatureException::class) suspend fun verify(): Boolean { - val request = withContext(Dispatchers.IO) { + val request = withContext(coroutineDispatchers.io) { machine.verifyDevice(inner.userId, inner.deviceId) } - this.sender.sendSignatureUpload(request) + sender.sendSignatureUpload(request) return true } @@ -151,20 +157,20 @@ internal class Device( * This will not fetch out fresh data from the Rust side. **/ internal fun toCryptoDeviceInfo(): CryptoDeviceInfo { - val keys = this.inner.keys.map { (keyId, key) -> "$keyId:$this.inner.deviceId" to key }.toMap() + val keys = inner.keys.map { (keyId, key) -> "$keyId:$inner.deviceId" to key }.toMap() return CryptoDeviceInfo( - this.inner.deviceId, - this.inner.userId, - this.inner.algorithms, - keys, + deviceId = inner.deviceId, + userId = inner.userId, + algorithms = inner.algorithms, + keys = keys, // The Kotlin side doesn't need to care about signatures, // so we're not filling this out - mapOf(), - UnsignedDeviceInfo(this.inner.displayName), - DeviceTrustLevel(crossSigningVerified = this.inner.crossSigningTrusted, locallyVerified = this.inner.locallyTrusted), - this.inner.isBlocked, + signatures = mapOf(), + unsigned = UnsignedDeviceInfo(inner.displayName), + trustLevel = DeviceTrustLevel(crossSigningVerified = inner.crossSigningTrusted, locallyVerified = inner.locallyTrusted), + isBlocked = inner.isBlocked, // TODO - null) + firstTimeSeenLocalTs = null) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index b3cb03d657..ea24ccc984 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -16,11 +16,13 @@ package org.matrix.android.sdk.internal.crypto -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.Dispatchers +import com.squareup.moshi.Moshi +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener @@ -36,6 +38,7 @@ import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.coroutines.builder.safeInvokeOnClose import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData @@ -44,8 +47,8 @@ import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo -import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.network.parsing.CheckNumberType import timber.log.Timber import uniffi.olm.BackupKeys @@ -64,7 +67,6 @@ import uniffi.olm.setLogger import java.io.File import java.nio.charset.Charset import java.util.UUID -import java.util.concurrent.ConcurrentHashMap import uniffi.olm.OlmMachine as InnerMachine import uniffi.olm.ProgressListener as RustProgressListener import uniffi.olm.UserIdentity as RustUserIdentity @@ -81,138 +83,71 @@ private class CryptoProgressListener(private val listener: ProgressListener?) : } } -internal class LiveDevice( - internal var userIds: List, - private var observer: DeviceUpdateObserver -) : MutableLiveData>() { +private data class UserIdentityCollector(val userId: String, val collector: SendChannel>) + : SendChannel> by collector +private data class DevicesCollector(val userIds: List, val collector: SendChannel>) + : SendChannel> by collector +private typealias PrivateKeysCollector = SendChannel> - override fun onActive() { - observer.addDeviceUpdateListener(this) - } - - override fun onInactive() { - observer.removeDeviceUpdateListener(this) - } -} - -internal class LiveUserIdentity( - internal var userId: String, - private var observer: UserIdentityUpdateObserver -) : MutableLiveData>() { - override fun onActive() { - observer.addUserIdentityUpdateListener(this) - } - - override fun onInactive() { - observer.removeUserIdentityUpdateListener(this) - } -} - -internal class LivePrivateCrossSigningKeys( - private var observer: PrivateCrossSigningKeysUpdateObserver, -) : MutableLiveData>() { - - override fun onActive() { - observer.addUserIdentityUpdateListener(this) - } - - override fun onInactive() { - observer.removeUserIdentityUpdateListener(this) - } +private class FlowCollectors { + val userIdentityCollectors = ArrayList() + val privateKeyCollectors = ArrayList() + val deviceCollectors = ArrayList() } fun setRustLogger() { setLogger(CryptoLogger() as Logger) } -internal class DeviceUpdateObserver { - internal val listeners = ConcurrentHashMap>() - - fun addDeviceUpdateListener(device: LiveDevice) { - listeners[device] = device.userIds - } - - fun removeDeviceUpdateListener(device: LiveDevice) { - listeners.remove(device) - } -} - -internal class UserIdentityUpdateObserver { - internal val listeners = ConcurrentHashMap() - - fun addUserIdentityUpdateListener(userIdentity: LiveUserIdentity) { - listeners[userIdentity] = userIdentity.userId - } - - fun removeUserIdentityUpdateListener(userIdentity: LiveUserIdentity) { - listeners.remove(userIdentity) - } -} - -internal class PrivateCrossSigningKeysUpdateObserver { - internal val listeners = ConcurrentHashMap() - - fun addUserIdentityUpdateListener(liveKeys: LivePrivateCrossSigningKeys) { - listeners[liveKeys] = Unit - } - - fun removeUserIdentityUpdateListener(liveKeys: LivePrivateCrossSigningKeys) { - listeners.remove(liveKeys) - } -} - internal class OlmMachine( user_id: String, device_id: String, path: File, - deviceObserver: DeviceUpdateObserver, private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val moshi: Moshi ) { - private val inner: InnerMachine = InnerMachine(user_id, device_id, path.toString()) - private val deviceUpdateObserver = deviceObserver - private val userIdentityUpdateObserver = UserIdentityUpdateObserver() - private val privateKeysUpdateObserver = PrivateCrossSigningKeysUpdateObserver() + private val inner: InnerMachine = InnerMachine(user_id, device_id, path.toString(), null) internal val verificationListeners = ArrayList() + private val flowCollectors = FlowCollectors() /** Get our own user ID. */ fun userId(): String { - return this.inner.userId() + return inner.userId() } /** Get our own device ID. */ fun deviceId(): String { - return this.inner.deviceId() + return inner.deviceId() } /** Get our own public identity keys ID. */ fun identityKeys(): Map { - return this.inner.identityKeys() + return inner.identityKeys() } fun inner(): InnerMachine { - return this.inner + return inner } - /** Update all of our live device listeners. */ private suspend fun updateLiveDevices() { - for ((liveDevice, users) in deviceUpdateObserver.listeners) { - val devices = getCryptoDeviceInfo(users) - liveDevice.postValue(devices) + for (deviceCollector in flowCollectors.deviceCollectors) { + val devices = getCryptoDeviceInfo(deviceCollector.userIds) + deviceCollector.trySend(devices) } } private suspend fun updateLiveUserIdentities() { - for ((liveIdentity, userId) in userIdentityUpdateObserver.listeners) { - val identity = getIdentity(userId)?.toMxCrossSigningInfo().toOptional() - liveIdentity.postValue(identity) + for (userIdentityCollector in flowCollectors.userIdentityCollectors) { + val identity = getIdentity(userIdentityCollector.userId)?.toMxCrossSigningInfo() + userIdentityCollector.trySend(identity.toOptional()) } } private suspend fun updateLivePrivateKeys() { - val keys = this.exportCrossSigningKeys().toOptional() - - for (liveKeys in privateKeysUpdateObserver.listeners.keys()) { - liveKeys.postValue(keys) + val keys = exportCrossSigningKeys().toOptional() + for (privateKeyCollector in flowCollectors.privateKeyCollectors) { + privateKeyCollector.trySend(keys) } } @@ -220,18 +155,18 @@ internal class OlmMachine( * Get our own device info as [CryptoDeviceInfo]. */ suspend fun ownDevice(): CryptoDeviceInfo { - val deviceId = this.deviceId() + val deviceId = deviceId() - val keys = this.identityKeys().map { (keyId, key) -> "$keyId:$deviceId" to key }.toMap() + val keys = identityKeys().map { (keyId, key) -> "$keyId:$deviceId" to key }.toMap() - val crossSigningVerified = when (val ownIdentity = this.getIdentity(this.userId())) { + val crossSigningVerified = when (val ownIdentity = getIdentity(userId())) { is OwnUserIdentity -> ownIdentity.trustsOurOwnDevice() else -> false } return CryptoDeviceInfo( - this.deviceId(), - this.userId(), + deviceId(), + userId(), // TODO pass the algorithms here. listOf(), keys, @@ -251,7 +186,7 @@ internal class OlmMachine( * @return the list of requests that needs to be sent to the homeserver */ suspend fun outgoingRequests(): List = - withContext(Dispatchers.IO) { inner.outgoingRequests() } + withContext(coroutineDispatchers.io) { inner.outgoingRequests() } /** * Mark a request that was sent to the server as sent. @@ -268,9 +203,8 @@ internal class OlmMachine( requestType: RequestType, responseBody: String ) = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { inner.markRequestAsSent(requestId, requestType, responseBody) - if (requestType == RequestType.KEYS_QUERY) { updateLiveDevices() updateLiveUserIdentities() @@ -296,7 +230,7 @@ internal class OlmMachine( deviceChanges: DeviceListResponse?, keyCounts: DeviceOneTimeKeysCountSyncResponse? ): ToDeviceSyncResponse { - val response = withContext(Dispatchers.IO) { + val response = withContext(coroutineDispatchers.io) { val counts: MutableMap = mutableMapOf() if (keyCounts?.signedCurve25519 != null) { @@ -306,7 +240,7 @@ internal class OlmMachine( val devices = DeviceLists(deviceChanges?.changed.orEmpty(), deviceChanges?.left.orEmpty()) val adapter = - MoshiProvider.providesMoshi().adapter(ToDeviceSyncResponse::class.java) + moshi.adapter(ToDeviceSyncResponse::class.java) val events = adapter.toJson(toDevice ?: ToDeviceSyncResponse())!! // TODO once our sync response type parses the unused fallback key @@ -315,11 +249,17 @@ internal class OlmMachine( } // We may get cross signing keys over a to-device event, update our listeners. - this.updateLivePrivateKeys() + updateLivePrivateKeys() return 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) + } + /** * Mark the given list of users to be tracked, triggering a key query request for them. * @@ -329,7 +269,7 @@ internal class OlmMachine( * @param users The users that should be queued up for a key query. */ suspend fun updateTrackedUsers(users: List) = - withContext(Dispatchers.IO) { inner.updateTrackedUsers(users) } + withContext(coroutineDispatchers.io) { inner.updateTrackedUsers(users) } /** * Check if the given user is considered to be tracked. @@ -338,7 +278,7 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) fun isUserTracked(userId: String): Boolean { - return this.inner.isUserTracked(userId) + return inner.isUserTracked(userId) } /** @@ -355,7 +295,7 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun getMissingSessions(users: List): Request? = - withContext(Dispatchers.IO) { inner.getMissingSessions(users) } + withContext(coroutineDispatchers.io) { inner.getMissingSessions(users) } /** * Share a room key with the given list of users for the given room. @@ -377,7 +317,7 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun shareRoomKey(roomId: String, users: List): List = - withContext(Dispatchers.IO) { inner.shareRoomKey(roomId, users) } + withContext(coroutineDispatchers.io) { inner.shareRoomKey(roomId, users) } /** * Encrypt the given event with the given type and content for the given room. @@ -411,8 +351,8 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun encrypt(roomId: String, eventType: String, content: Content): Content = - withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Map::class.java) val contentString = adapter.toJson(content) val encrypted = inner.encrypt(roomId, eventType, contentString) adapter.fromJson(encrypted)!! @@ -429,17 +369,17 @@ internal class OlmMachine( */ @Throws(MXCryptoError::class) suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult = - withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) - val serializedEvent = adapter.toJson(event) + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Event::class.java) try { if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } + val serializedEvent = adapter.toJson(event) val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId) val deserializationAdapter = - MoshiProvider.providesMoshi().adapter(Map::class.java) + moshi.adapter(Map::class.java) val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent) ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) @@ -469,8 +409,8 @@ internal class OlmMachine( */ @Throws(DecryptionException::class) suspend fun requestRoomKey(event: Event): KeyRequestPair = - withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Event::class.java) val serializedEvent = adapter.toJson(event) inner.requestRoomKey(serializedEvent, event.roomId!!) @@ -488,7 +428,7 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray = - withContext(Dispatchers.IO) { inner.exportKeys(passphrase, rounds).toByteArray() } + withContext(coroutineDispatchers.io) { inner.exportKeys(passphrase, rounds).toByteArray() } /** * Import room keys from the given serialized key export. @@ -505,7 +445,7 @@ internal class OlmMachine( passphrase: String, listener: ProgressListener? ): ImportRoomKeysResult = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { val decodedKeys = String(keys, Charset.defaultCharset()) val rustListener = CryptoProgressListener(listener) @@ -520,8 +460,8 @@ internal class OlmMachine( keys: List, listener: ProgressListener? ): ImportRoomKeysResult = - withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(List::class.java) + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(List::class.java) // If the key backup is too big we take the risk of causing OOM // when serializing to json @@ -550,25 +490,31 @@ internal class OlmMachine( @Throws(CryptoStoreException::class) suspend fun getIdentity(userId: String): UserIdentities? { - val identity = withContext(Dispatchers.IO) { + val identity = withContext(coroutineDispatchers.io) { inner.getIdentity(userId) } - val adapter = MoshiProvider.providesMoshi().adapter(RestKeyInfo::class.java) + val adapter = moshi.adapter(RestKeyInfo::class.java) return when (identity) { is RustUserIdentity.Other -> { - val verified = this.inner().isIdentityVerified(userId) + val verified = inner().isIdentityVerified(userId) val masterKey = adapter.fromJson(identity.masterKey)!!.toCryptoModel().apply { trustLevel = DeviceTrustLevel(verified, verified) } val selfSigningKey = adapter.fromJson(identity.selfSigningKey)!!.toCryptoModel().apply { trustLevel = DeviceTrustLevel(verified, verified) } - - UserIdentity(identity.userId, masterKey, selfSigningKey, this, this.requestSender) + UserIdentity( + userId = identity.userId, + masterKey = masterKey, + selfSigningKey = selfSigningKey, + olmMachine = this, + requestSender = requestSender, + coroutineDispatchers = coroutineDispatchers + ) } is RustUserIdentity.Own -> { - val verified = this.inner().isIdentityVerified(userId) + val verified = inner().isIdentityVerified(userId) val masterKey = adapter.fromJson(identity.masterKey)!!.toCryptoModel().apply { trustLevel = DeviceTrustLevel(verified, verified) @@ -579,13 +525,14 @@ internal class OlmMachine( val userSigningKey = adapter.fromJson(identity.userSigningKey)!!.toCryptoModel() OwnUserIdentity( - identity.userId, - masterKey, - selfSigningKey, - userSigningKey, - identity.trustsOurOwnDevice, - this, - this.requestSender + userId = identity.userId, + masterKey = masterKey, + selfSigningKey = selfSigningKey, + userSigningKey = userSigningKey, + trustsOurOwnDevice = identity.trustsOurOwnDevice, + olmMachine = this, + requestSender = requestSender, + coroutineDispatchers = coroutineDispatchers ) } null -> null @@ -605,27 +552,35 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun getCryptoDeviceInfo(userId: String, deviceId: String): CryptoDeviceInfo? { - return if (userId == userId() && deviceId == deviceId()) { - // Our own device isn't part of our store on the Rust side, return it - // using our ownDevice method - ownDevice() - } else { - getDevice(userId, deviceId)?.toCryptoDeviceInfo() - } + return getDevice(userId, deviceId)?.toCryptoDeviceInfo() } @Throws(CryptoStoreException::class) suspend fun getDevice(userId: String, deviceId: String): Device? { - val device = withContext(Dispatchers.IO) { + val device = withContext(coroutineDispatchers.io) { inner.getDevice(userId, deviceId) } ?: return null - return Device(this.inner, device, this.requestSender, this.verificationListeners) + return Device( + machine = inner, + inner = device, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners + ) } suspend fun getUserDevices(userId: String): List { - return withContext(Dispatchers.IO) { - inner.getUserDevices(userId).map { Device(inner, it, requestSender, verificationListeners) } + return withContext(coroutineDispatchers.io) { + inner.getUserDevices(userId).map { + Device( + machine = inner, + inner = it, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners + ) + } } } @@ -638,15 +593,17 @@ internal class OlmMachine( */ @Throws(CryptoStoreException::class) suspend fun getCryptoDeviceInfo(userId: String): List { - val devices = this.getUserDevices(userId).map { it.toCryptoDeviceInfo() }.toMutableList() - + return getUserDevices(userId).map { it.toCryptoDeviceInfo() } +/* // EA doesn't differentiate much between our own and other devices of // while the rust-sdk does, append our own device here. - if (userId == this.userId()) { - devices.add(this.ownDevice()) + if (userId == userId()) { + devices.add(ownDevice()) } return devices + + */ } /** @@ -660,7 +617,7 @@ internal class OlmMachine( val plainDevices: ArrayList = arrayListOf() for (user in userIds) { - val devices = this.getCryptoDeviceInfo(user) + val devices = getCryptoDeviceInfo(user) plainDevices.addAll(devices) } @@ -669,7 +626,7 @@ internal class OlmMachine( @Throws suspend fun forceKeyDownload(userIds: List) { - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { val requestId = UUID.randomUUID().toString() val response = requestSender.queryKeys(Request.KeysQuery(requestId, userIds)) markRequestAsSent(requestId, RequestType.KEYS_QUERY, response) @@ -680,7 +637,7 @@ internal class OlmMachine( val userMap = MXUsersDevicesMap() for (user in userIds) { - val devices = this.getCryptoDeviceInfo(user) + val devices = getCryptoDeviceInfo(user) for (device in devices) { userMap.setObject(user, device.deviceId, device) @@ -697,7 +654,18 @@ internal class OlmMachine( * The key query request will be retried a few time in case of shaky connection, but could fail. */ suspend fun ensureUserDevicesMap(userIds: List, forceDownload: Boolean = false): MXUsersDevicesMap { - val toDownload = if (forceDownload) { + ensureUsersKeys(userIds, forceDownload) + return getUserDevicesMap(userIds) + } + + /** + * If the user is untracked or forceDownload is set to true, a key query request will be made. + * It will suspend until query response. + * + * The key query request will be retried a few time in case of shaky connection, but could fail. + */ + suspend fun ensureUsersKeys(userIds: List, forceDownload: Boolean = false) { + val userIdsToFetchKeys = if (forceDownload) { userIds } else { userIds.mapNotNull { userId -> @@ -706,26 +674,34 @@ internal class OlmMachine( updateTrackedUsers(it) } } - tryOrNull("Failed to download keys for $toDownload") { - forceKeyDownload(toDownload) + tryOrNull("Failed to download keys for $userIdsToFetchKeys") { + forceKeyDownload(userIdsToFetchKeys) } - return getUserDevicesMap(userIds) } - suspend fun getLiveUserIdentity(userId: String): LiveData> { - val identity = this.getIdentity(userId)?.toMxCrossSigningInfo().toOptional() - val liveIdentity = LiveUserIdentity(userId, this.userIdentityUpdateObserver) - liveIdentity.value = identity - - return liveIdentity + fun getLiveUserIdentity(userId: String): Flow> { + return channelFlow { + val userIdentityCollector = UserIdentityCollector(userId, this) + val onClose = safeInvokeOnClose { + flowCollectors.userIdentityCollectors.remove(userIdentityCollector) + } + flowCollectors.userIdentityCollectors.add(userIdentityCollector) + val identity = getIdentity(userId)?.toMxCrossSigningInfo().toOptional() + send(identity) + onClose.await() + } } - suspend fun getLivePrivateCrossSigningKeys(): LiveData> { - val keys = this.exportCrossSigningKeys().toOptional() - val liveKeys = LivePrivateCrossSigningKeys(this.privateKeysUpdateObserver) - liveKeys.value = keys - - return liveKeys + fun getLivePrivateCrossSigningKeys(): Flow> { + return channelFlow { + val onClose = safeInvokeOnClose { + flowCollectors.privateKeyCollectors.remove(this) + } + flowCollectors.privateKeyCollectors.add(this) + val keys = this@OlmMachine.exportCrossSigningKeys().toOptional() + send(keys) + onClose.await() + } } /** @@ -736,14 +712,19 @@ internal class OlmMachine( * * @param userIds The ids of the device owners. * - * @return The list of Devices or an empty list if there aren't any. + * @return The list of Devices or an empty list if there aren't any as a Flow. */ - suspend fun getLiveDevices(userIds: List): LiveData> { - val plainDevices = getCryptoDeviceInfo(userIds) - val devices = LiveDevice(userIds, deviceUpdateObserver) - devices.value = plainDevices - - return devices + fun getLiveDevices(userIds: List): Flow> { + return channelFlow { + val devicesCollector = DevicesCollector(userIds, this) + val onClose = safeInvokeOnClose { + flowCollectors.deviceCollectors.remove(devicesCollector) + } + flowCollectors.deviceCollectors.add(devicesCollector) + val devices = getCryptoDeviceInfo(userIds) + send(devices) + onClose.await() + } } /** Discard the currently active room key for the given room if there is one. */ @@ -760,26 +741,27 @@ internal class OlmMachine( * @return The list of [VerificationRequest] that we share with the given user */ fun getVerificationRequests(userId: String): List { - return this.inner.getVerificationRequests(userId).map { + return inner.getVerificationRequests(userId).map { VerificationRequest( - this.inner, - it, - this.requestSender, - this.verificationListeners, + machine = inner, + inner = it, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners, ) } } /** Get a verification request for the given user with the given flow ID */ fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { - val request = this.inner.getVerificationRequest(userId, flowId) - + val request = inner.getVerificationRequest(userId, flowId) return if (request != null) { VerificationRequest( - this.inner, - request, - requestSender, - this.verificationListeners, + machine = inner, + inner = request, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners, ) } else { null @@ -792,13 +774,26 @@ internal class OlmMachine( * verification. */ fun getVerification(userId: String, flowId: String): VerificationTransaction? { - return when (val verification = this.inner.getVerification(userId, flowId)) { + return when (val verification = inner.getVerification(userId, flowId)) { is uniffi.olm.Verification.QrCodeV1 -> { - val request = this.getVerificationRequest(userId, flowId) ?: return null - QrCodeVerification(inner, request, verification.qrcode, requestSender, verificationListeners) + val request = getVerificationRequest(userId, flowId) ?: return null + QrCodeVerification( + machine = inner, + request = request, + inner = verification.qrcode, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners + ) } is uniffi.olm.Verification.SasV1 -> { - SasVerification(inner, verification.sas, requestSender, verificationListeners) + SasVerification( + machine = inner, + inner = verification.sas, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners + ) } null -> { // This branch exists because scanning a QR code is tied to the QrCodeVerification, @@ -808,7 +803,14 @@ internal class OlmMachine( val request = getVerificationRequest(userId, flowId) ?: return null if (request.canScanQrCodes()) { - QrCodeVerification(inner, request, null, requestSender, verificationListeners) + QrCodeVerification( + machine = inner, + request = request, + inner = null, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = verificationListeners + ) } else { null } @@ -817,23 +819,22 @@ internal class OlmMachine( } suspend fun bootstrapCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { - val requests = withContext(Dispatchers.IO) { + val requests = withContext(coroutineDispatchers.io) { inner.bootstrapCrossSigning() } - - this.requestSender.uploadCrossSigningKeys(requests.uploadSigningKeysRequest, uiaInterceptor) - this.requestSender.sendSignatureUpload(requests.signatureRequest) + requestSender.uploadCrossSigningKeys(requests.uploadSigningKeysRequest, uiaInterceptor) + requestSender.sendSignatureUpload(requests.signatureRequest) } /** * Get the status of our private cross signing keys, i.e. which private keys do we have stored locally. */ fun crossSigningStatus(): CrossSigningStatus { - return this.inner.crossSigningStatus() + return inner.crossSigningStatus() } suspend fun exportCrossSigningKeys(): PrivateKeysInfo? { - val export = withContext(Dispatchers.IO) { + val export = withContext(coroutineDispatchers.io) { inner.exportCrossSigningKeys() } ?: return null @@ -844,7 +845,7 @@ internal class OlmMachine( val rustExport = CrossSigningKeyExport(export.master, export.selfSigned, export.user) var result: UserTrustResult - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { result = try { inner.importCrossSigningKeys(rustExport) @@ -861,21 +862,21 @@ internal class OlmMachine( UserTrustResult.Failure(failure.localizedMessage) } } - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { this@OlmMachine.updateLivePrivateKeys() } return result } suspend fun sign(message: String): Map> { - return withContext(Dispatchers.Default) { + return withContext(coroutineDispatchers.computation) { inner.sign(message) } } @Throws(CryptoStoreException::class) suspend fun enableBackupV1(key: String, version: String) { - return withContext(Dispatchers.Default) { + return withContext(coroutineDispatchers.computation) { val backupKey = MegolmV1BackupKey(key, mapOf(), null, MXCRYPTO_ALGORITHM_MEGOLM_BACKUP) inner.enableBackupV1(backupKey, version) } @@ -891,26 +892,29 @@ internal class OlmMachine( } @Throws(CryptoStoreException::class) - fun roomKeyCounts(): RoomKeyCounts { - // TODO convert this to a suspendable method - return inner.roomKeyCounts() + suspend fun roomKeyCounts(): RoomKeyCounts { + return withContext(coroutineDispatchers.computation) { + inner.roomKeyCounts() + } } @Throws(CryptoStoreException::class) - fun getBackupKeys(): BackupKeys? { - // TODO this needs to be suspendable - return inner.getBackupKeys() + suspend fun getBackupKeys(): BackupKeys? { + return withContext(coroutineDispatchers.computation) { + inner.getBackupKeys() + } } @Throws(CryptoStoreException::class) - fun saveRecoveryKey(key: String?, version: String?) { - // TODO convert this to a suspendable method - inner.saveRecoveryKey(key, version) + suspend fun saveRecoveryKey(key: String?, version: String?) { + withContext(coroutineDispatchers.computation) { + inner.saveRecoveryKey(key, version) + } } @Throws(CryptoStoreException::class) suspend fun backupRoomKeys(): Request? { - return withContext(Dispatchers.Default) { + return withContext(coroutineDispatchers.computation) { Timber.d("BACKUP CREATING REQUEST") val request = inner.backupRoomKeys() Timber.d("BACKUP CREATED REQUEST: $request") @@ -920,9 +924,8 @@ internal class OlmMachine( @Throws(CryptoStoreException::class) suspend fun checkAuthDataSignature(authData: MegolmBackupAuthData): Boolean { - return withContext(Dispatchers.Default) { - val adapter = MoshiProvider - .providesMoshi() + return withContext(coroutineDispatchers.computation) { + val adapter = moshi .newBuilder() .add(CheckNumberType.JSON_ADAPTER_FACTORY) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt index 441fe5acd0..a46fcddcaf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.crypto +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.UserId @@ -28,10 +31,16 @@ internal class OlmMachineProvider @Inject constructor( @UserId private val userId: String, @DeviceId private val deviceId: String?, @SessionFilesDirectory private val dataDir: File, - requestSender: RequestSender + requestSender: RequestSender, + coroutineDispatchers: MatrixCoroutineDispatchers, + moshi: Moshi ) { - private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver() - - var olmMachine: OlmMachine = OlmMachine(userId, deviceId!!, dataDir, deviceObserver, requestSender) + var olmMachine: OlmMachine = OlmMachine( + user_id = userId, + device_id = deviceId!!, + path = dataDir, + requestSender = requestSender, + coroutineDispatchers = coroutineDispatchers, + moshi = moshi) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/QrCodeVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/QrCodeVerification.kt index e61c5c8f8f..b6e5cdd8a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/QrCodeVerification.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/QrCodeVerification.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.UpdateDispatcher import uniffi.olm.CryptoStoreException import uniffi.olm.OlmMachine @@ -37,13 +37,15 @@ internal class QrCodeVerification( private var request: VerificationRequest, private var inner: QrCode?, private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, listeners: ArrayList ) : QrCodeVerificationTransaction { + private val dispatcher = UpdateDispatcher(listeners) private fun dispatchTxUpdated() { refreshData() - this.dispatcher.dispatchTxUpdated(this) + dispatcher.dispatchTxUpdated(this) } /** Generate, if possible, data that should be encoded as a QR code for QR code verification. @@ -60,27 +62,25 @@ internal class QrCodeVerification( */ override val qrCodeText: String? get() { - val data = this.inner?.let { this.machine.generateQrCode(it.otherUserId, it.flowId) } + val data = inner?.let { machine.generateQrCode(it.otherUserId, it.flowId) } // TODO Why are we encoding this to ISO_8859_1? If we're going to encode, why not base64? return data?.fromBase64()?.toString(Charsets.ISO_8859_1) } /** Pass the data from a scanned QR code into the QR code verification object */ - override fun userHasScannedOtherQrCode(otherQrCodeText: String) { - runBlocking { - request.scanQrCode(otherQrCodeText) - } + override suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) { + request.scanQrCode(otherQrCodeText) dispatchTxUpdated() } /** Confirm that the other side has indeed scanned the QR code we presented */ - override fun otherUserScannedMyQrCode() { - runBlocking { confirm() } + override suspend fun otherUserScannedMyQrCode() { + confirm() } /** Cancel the QR code verification, denying that the other side has scanned the QR code */ - override fun otherUserDidNotScannedMyQrCode() { + override suspend fun otherUserDidNotScannedMyQrCode() { // TODO Is this code correct here? The old code seems to do this cancelHelper(CancelCode.MismatchedKeys) } @@ -88,7 +88,7 @@ internal class QrCodeVerification( override var state: VerificationTxState get() { refreshData() - val inner = this.inner + val inner = inner val cancelInfo = inner?.cancelInfo return if (inner != null) { @@ -114,22 +114,22 @@ internal class QrCodeVerification( /** Get the unique id of this verification */ override val transactionId: String - get() = this.request.flowId() + get() = request.flowId() /** Get the user id of the other user participating in this verification flow */ override val otherUserId: String - get() = this.request.otherUser() + get() = request.otherUser() /** Get the device id of the other user's device participating in this verification flow */ override var otherDeviceId: String? - get() = this.request.otherDeviceId() + get() = request.otherDeviceId() @Suppress("UNUSED_PARAMETER") set(value) { } /** Did the other side initiate this verification flow */ override val isIncoming: Boolean - get() = !this.request.weStarted() + get() = !request.weStarted() /** Cancel the verification flow * @@ -140,7 +140,7 @@ internal class QrCodeVerification( * * The method turns into a noop, if the verification flow has already been cancelled. * */ - override fun cancel() { + override suspend fun cancel() { cancelHelper(CancelCode.User) } @@ -155,13 +155,13 @@ internal class QrCodeVerification( * * @param code The cancel code that should be given as the reason for the cancellation. * */ - override fun cancel(code: CancelCode) { + override suspend fun cancel(code: CancelCode) { cancelHelper(code) } /** Is this verification happening over to-device messages */ override fun isToDeviceTransport(): Boolean { - return this.request.roomId() == null + return request.roomId() == null } /** Confirm the QR code verification @@ -174,36 +174,36 @@ internal class QrCodeVerification( */ @Throws(CryptoStoreException::class) private suspend fun confirm() { - val result = withContext(Dispatchers.IO) { + val result = withContext(coroutineDispatchers.io) { machine.confirmVerification(request.otherUser(), request.flowId()) } if (result != null) { - this.sender.sendVerificationRequest(result.request) - dispatchTxUpdated() - - val signatureRequest = result.signatureRequest - - if (signatureRequest != null) { - this.sender.sendSignatureUpload(signatureRequest) + for (verificationRequest in result.requests) { + sender.sendVerificationRequest(verificationRequest) } + val signatureRequest = result.signatureRequest + if (signatureRequest != null) { + sender.sendSignatureUpload(signatureRequest) + } + dispatchTxUpdated() } } - private fun cancelHelper(code: CancelCode) { - val request = this.machine.cancelVerification(this.request.otherUser(), this.request.flowId(), code.value) + private suspend fun cancelHelper(code: CancelCode) { + val request = machine.cancelVerification(request.otherUser(), request.flowId(), code.value) if (request != null) { - runBlocking { sender.sendVerificationRequest(request) } + sender.sendVerificationRequest(request) dispatchTxUpdated() } } /** Fetch fresh data from the Rust side for our verification flow */ private fun refreshData() { - when (val verification = this.machine.getVerification(this.request.otherUser(), this.request.flowId())) { + when (val verification = machine.getVerification(request.otherUser(), request.flowId())) { is Verification.QrCodeV1 -> { - this.inner = verification.qrcode + inner = verification.qrcode } else -> { } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt index 07918870d3..f20034e9c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt @@ -16,10 +16,7 @@ package org.matrix.android.sdk.internal.crypto -import androidx.lifecycle.LiveData -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.NoOpMatrixCallback +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.extensions.orFalse @@ -31,7 +28,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback import javax.inject.Inject internal class RustCrossSigningService @Inject constructor( @@ -45,30 +41,30 @@ internal class RustCrossSigningService @Inject constructor( /** * Is our own device signed by our own cross signing identity */ - override fun isCrossSigningVerified(): Boolean { - return when (val identity = runBlocking { olmMachine.getIdentity(olmMachine.userId()) }) { + override suspend fun isCrossSigningVerified(): Boolean { + return when (val identity = olmMachine.getIdentity(olmMachine.userId())) { is OwnUserIdentity -> identity.trustsOurOwnDevice() else -> false } } - override fun isUserTrusted(otherUserId: String): Boolean { + override suspend fun isUserTrusted(otherUserId: String): Boolean { // This seems to be used only in tests. - return this.checkUserTrust(otherUserId).isVerified() + return checkUserTrust(otherUserId).isVerified() } /** * Will not force a download of the key, but will verify signatures trust chain. * Checks that my trusted user key has signed the other user UserKey */ - override fun checkUserTrust(otherUserId: String): UserTrustResult { - val identity = runBlocking { olmMachine.getIdentity(olmMachine.userId()) } + override suspend fun checkUserTrust(otherUserId: String): UserTrustResult { + val identity = olmMachine.getIdentity(olmMachine.userId()) // While UserTrustResult has many different states, they are by the callers // converted to a boolean value immediately, thus we don't need to support // all the values. return if (identity != null) { - val verified = runBlocking { identity.verified() } + val verified = identity.verified() if (verified) { UserTrustResult.Success @@ -84,8 +80,8 @@ internal class RustCrossSigningService @Inject constructor( * Initialize cross signing for this user. * Users needs to enter credentials */ - override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { - runBlocking { runCatching { olmMachine.bootstrapCrossSigning(uiaInterceptor) }.foldToCallback(callback) } + override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { + olmMachine.bootstrapCrossSigning(uiaInterceptor) } /** @@ -108,26 +104,26 @@ internal class RustCrossSigningService @Inject constructor( * * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. */ - override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return runBlocking { olmMachine.getIdentity(otherUserId)?.toMxCrossSigningInfo() } + override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return olmMachine.getIdentity(otherUserId)?.toMxCrossSigningInfo() } - override fun getLiveCrossSigningKeys(userId: String): LiveData> { - return runBlocking { olmMachine.getLiveUserIdentity(userId) } + override fun getLiveCrossSigningKeys(userId: String): Flow> { + return olmMachine.getLiveUserIdentity(userId) } /** Get our own public cross signing keys */ - override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? { return getUserCrossSigningKeys(olmMachine.userId()) } /** Get our own private cross signing keys */ - override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return runBlocking { olmMachine.exportCrossSigningKeys() } + override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return olmMachine.exportCrossSigningKeys() } - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - return runBlocking { olmMachine.getLivePrivateCrossSigningKeys() } + override fun getLiveCrossSigningPrivateKeys(): Flow> { + return olmMachine.getLivePrivateCrossSigningKeys() } /** @@ -136,34 +132,32 @@ internal class RustCrossSigningService @Inject constructor( * Returning true means that we have the private self-signing and user-signing keys at hand. */ override fun canCrossSign(): Boolean { - val status = this.olmMachine.crossSigningStatus() + val status = olmMachine.crossSigningStatus() return status.hasSelfSigning && status.hasUserSigning } override fun allPrivateKeysKnown(): Boolean { - val status = this.olmMachine.crossSigningStatus() + val status = olmMachine.crossSigningStatus() return status.hasMaster && status.hasSelfSigning && status.hasUserSigning } /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server */ - override fun trustUser(otherUserId: String, callback: MatrixCallback) { + override suspend fun trustUser(otherUserId: String) { // This is only used in a test - val userIdentity = runBlocking { olmMachine.getIdentity(otherUserId) } - + val userIdentity = olmMachine.getIdentity(otherUserId) if (userIdentity != null) { - runBlocking { userIdentity.verify() } - callback.onSuccess(Unit) + userIdentity.verify() } else { - callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + throw Throwable("## CrossSigning - CrossSigning is not setup for this account") } } /** Mark our own master key as trusted */ - override fun markMyMasterKeyAsTrusted() { + override suspend fun markMyMasterKeyAsTrusted() { // This doesn't seem to be used? - this.trustUser(this.olmMachine.userId(), NoOpMatrixCallback()) + trustUser(olmMachine.userId()) } /** @@ -171,10 +165,8 @@ internal class RustCrossSigningService @Inject constructor( */ override suspend fun trustDevice(deviceId: String) { val device = olmMachine.getDevice(olmMachine.userId(), deviceId) - - return if (device != null) { + if (device != null) { val verified = device.verify() - if (verified) { return } else { @@ -192,15 +184,15 @@ internal class RustCrossSigningService @Inject constructor( * using the self-signing key for our own devices or using the user-signing key and the master * key of another user. */ - override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { - val device = runBlocking { olmMachine.getDevice(otherUserId, otherDeviceId) } + override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val device = olmMachine.getDevice(otherUserId, otherDeviceId) return if (device != null) { // TODO i don't quite understand the semantics here and there are no docs for // DeviceTrustResult, what do the different result types mean exactly, // do you return success only if the Device is cross signing verified? // what about the local trust if it isn't? why is the local trust passed as an argument here? - DeviceTrustResult.Success(runBlocking { device.trustLevel() }) + DeviceTrustResult.Success(device.trustLevel()) } else { DeviceTrustResult.UnknownDevice(otherDeviceId) } @@ -215,7 +207,7 @@ internal class RustCrossSigningService @Inject constructor( } override fun onSecretUSKGossip(uskPrivateKey: String) { - // And this. + // And } override suspend fun shieldForGroup(userIds: List): RoomEncryptionTrustLevel { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SasVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SasVerification.kt index 11a00c3b64..89a179ea09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SasVerification.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SasVerification.kt @@ -16,15 +16,15 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.UpdateDispatcher import org.matrix.android.sdk.internal.crypto.verification.getEmojiForCode import uniffi.olm.CryptoStoreException @@ -37,6 +37,7 @@ internal class SasVerification( private val machine: OlmMachine, private var inner: Sas, private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, listeners: ArrayList ) : SasVerificationTransaction { @@ -44,27 +45,27 @@ internal class SasVerification( private fun dispatchTxUpdated() { refreshData() - this.dispatcher.dispatchTxUpdated(this) + dispatcher.dispatchTxUpdated(this) } /** The user ID of the other user that is participating in this verification flow */ - override val otherUserId: String = this.inner.otherUserId + override val otherUserId: String = inner.otherUserId /** Get the device id of the other user's device participating in this verification flow */ override var otherDeviceId: String? - get() = this.inner.otherDeviceId + get() = inner.otherDeviceId @Suppress("UNUSED_PARAMETER") set(value) { } /** Did the other side initiate this verification flow */ override val isIncoming: Boolean - get() = !this.inner.weStarted + get() = !inner.weStarted override var state: VerificationTxState get() { refreshData() - val cancelInfo = this.inner.cancelInfo + val cancelInfo = inner.cancelInfo return when { cancelInfo != null -> { @@ -84,7 +85,7 @@ internal class SasVerification( /** Get the unique id of this verification */ override val transactionId: String - get() = this.inner.flowId + get() = inner.flowId /** Cancel the verification flow * @@ -95,8 +96,8 @@ internal class SasVerification( * * The method turns into a noop, if the verification flow has already been cancelled. * */ - override fun cancel() { - this.cancelHelper(CancelCode.User) + override suspend fun cancel() { + cancelHelper(CancelCode.User) } /** Cancel the verification flow @@ -110,8 +111,8 @@ internal class SasVerification( * * @param code The cancel code that should be given as the reason for the cancellation. * */ - override fun cancel(code: CancelCode) { - this.cancelHelper(code) + override suspend fun cancel(code: CancelCode) { + cancelHelper(code) } /** Cancel the verification flow @@ -123,25 +124,17 @@ internal class SasVerification( * * The method turns into a noop, if the verification flow has already been cancelled. */ - override fun shortCodeDoesNotMatch() { - this.cancelHelper(CancelCode.MismatchedSas) + override suspend fun shortCodeDoesNotMatch() { + cancelHelper(CancelCode.MismatchedSas) } /** Is this verification happening over to-device messages */ - override fun isToDeviceTransport(): Boolean = this.inner.roomId == null - - /** Does the verification flow support showing decimals as the short auth string */ - override fun supportsDecimal(): Boolean { - // This is ignored anyways, throw it away? - // The spec also mandates that devices support at least decimal and - // the rust-sdk cancels if devices don't support it - return true - } + override fun isToDeviceTransport(): Boolean = inner.roomId == null /** Does the verification flow support showing emojis as the short auth string */ override fun supportsEmoji(): Boolean { refreshData() - return this.inner.supportsEmoji + return inner.supportsEmoji } /** Confirm that the short authentication code matches on both sides @@ -153,8 +146,8 @@ internal class SasVerification( * This method is a noop if we're not yet in a presentable state, i.e. we didn't receive * a m.key.verification.key event from the other side or we're cancelled. */ - override fun userHasVerifiedShortCode() { - runBlocking { confirm() } + override suspend fun userHasVerifiedShortCode() { + confirm() } /** Accept the verification flow, signaling the other side that we do want to verify @@ -165,8 +158,8 @@ internal class SasVerification( * This method is a noop if we send the start event out or if the verification has already * been accepted. */ - override fun acceptVerification() { - runBlocking { accept() } + override suspend fun acceptVerification() { + accept() } /** Get the decimal representation of the short auth string @@ -176,7 +169,7 @@ internal class SasVerification( * in a presentable state. */ override fun getDecimalCodeRepresentation(): String { - val decimals = this.machine.getDecimals(this.inner.otherUserId, this.inner.flowId) + val decimals = machine.getDecimals(inner.otherUserId, inner.flowId) return decimals?.joinToString(" ") ?: "" } @@ -188,52 +181,51 @@ internal class SasVerification( * state. */ override fun getEmojiCodeRepresentation(): List { - val emojiIndex = this.machine.getEmojiIndex(this.inner.otherUserId, this.inner.flowId) + val emojiIndex = machine.getEmojiIndex(inner.otherUserId, inner.flowId) return emojiIndex?.map { getEmojiForCode(it) } ?: listOf() } internal suspend fun accept() { - val request = this.machine.acceptSasVerification(this.inner.otherUserId, inner.flowId) + val request = machine.acceptSasVerification(inner.otherUserId, inner.flowId) if (request != null) { - this.sender.sendVerificationRequest(request) + sender.sendVerificationRequest(request) dispatchTxUpdated() } } @Throws(CryptoStoreException::class) private suspend fun confirm() { - val result = withContext(Dispatchers.IO) { + val result = withContext(coroutineDispatchers.io) { machine.confirmVerification(inner.otherUserId, inner.flowId) } - if (result != null) { - this.sender.sendVerificationRequest(result.request) - dispatchTxUpdated() - - val signatureRequest = result.signatureRequest - - if (signatureRequest != null) { - this.sender.sendSignatureUpload(signatureRequest) + for (verificationRequest in result.requests) { + sender.sendVerificationRequest(verificationRequest) } + val signatureRequest = result.signatureRequest + if (signatureRequest != null) { + sender.sendSignatureUpload(signatureRequest) + } + dispatchTxUpdated() } } - private fun cancelHelper(code: CancelCode) { - val request = this.machine.cancelVerification(this.inner.otherUserId, inner.flowId, code.value) + private suspend fun cancelHelper(code: CancelCode) { + val request = machine.cancelVerification(inner.otherUserId, inner.flowId, code.value) if (request != null) { - runBlocking { sender.sendVerificationRequest(request) } + sender.sendVerificationRequest(request) dispatchTxUpdated() } } /** Fetch fresh data from the Rust side for our verification flow */ private fun refreshData() { - when (val verification = this.machine.getVerification(this.inner.otherUserId, this.inner.flowId)) { + when (val verification = machine.getVerification(inner.otherUserId, inner.flowId)) { is Verification.SasV1 -> { - this.inner = verification.sas + inner = verification.sas } else -> { } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt index 733d4a5e1f..3cc94dae86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt @@ -16,14 +16,14 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.prepareMethods import uniffi.olm.CryptoStoreException import uniffi.olm.SignatureException @@ -65,7 +65,7 @@ sealed class UserIdentities { /** * Convert the identity into a MxCrossSigningInfo class. */ - abstract fun toMxCrossSigningInfo(): MXCrossSigningInfo + abstract suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo } /** @@ -80,11 +80,12 @@ internal class OwnUserIdentity( private val userSigningKey: CryptoCrossSigningKey, private val trustsOurOwnDevice: Boolean, private val olmMachine: OlmMachine, - private val requestSender: RequestSender) : UserIdentities() { + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers) : UserIdentities() { /** * Our own user id. */ - override fun userId() = this.userId + override fun userId() = userId /** * Manually verify our user identity. @@ -95,8 +96,8 @@ internal class OwnUserIdentity( */ @Throws(SignatureException::class) override suspend fun verify() { - val request = withContext(Dispatchers.Default) { olmMachine.inner().verifyIdentity(userId) } - this.requestSender.sendSignatureUpload(request) + val request = withContext(coroutineDispatchers.computation) { olmMachine.inner().verifyIdentity(userId) } + requestSender.sendSignatureUpload(request) } /** @@ -106,13 +107,13 @@ internal class OwnUserIdentity( */ @Throws(CryptoStoreException::class) override suspend fun verified(): Boolean { - return withContext(Dispatchers.IO) { olmMachine.inner().isIdentityVerified(userId) } + return withContext(coroutineDispatchers.io) { olmMachine.inner().isIdentityVerified(userId) } } /** * Does the identity trust our own device. */ - fun trustsOurOwnDevice() = this.trustsOurOwnDevice + fun trustsOurOwnDevice() = trustsOurOwnDevice /** * Request an interactive verification to begin @@ -133,32 +134,33 @@ internal class OwnUserIdentity( @Throws(CryptoStoreException::class) suspend fun requestVerification(methods: List): VerificationRequest { val stringMethods = prepareMethods(methods) - val result = this.olmMachine.inner().requestSelfVerification(stringMethods) - this.requestSender.sendVerificationRequest(result!!.request) + val result = olmMachine.inner().requestSelfVerification(stringMethods) + requestSender.sendVerificationRequest(result!!.request) return VerificationRequest( - this.olmMachine.inner(), - result.verification, - this.requestSender, - this.olmMachine.verificationListeners + machine = olmMachine.inner(), + inner = result.verification, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = olmMachine.verificationListeners ) } /** * Convert the identity into a MxCrossSigningInfo class. */ - override fun toMxCrossSigningInfo(): MXCrossSigningInfo { - val masterKey = this.masterKey - val selfSigningKey = this.selfSigningKey - val userSigningKey = this.userSigningKey - val trustLevel = DeviceTrustLevel(runBlocking { verified() }, false) + override suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo { + val masterKey = masterKey + val selfSigningKey = selfSigningKey + val userSigningKey = userSigningKey + val trustLevel = DeviceTrustLevel(verified(), false) // TODO remove this, this is silly, we have way too many methods to check if a user is verified masterKey.trustLevel = trustLevel selfSigningKey.trustLevel = trustLevel userSigningKey.trustLevel = trustLevel val crossSigningKeys = listOf(masterKey, selfSigningKey, userSigningKey) - return MXCrossSigningInfo(this.userId, crossSigningKeys) + return MXCrossSigningInfo(userId, crossSigningKeys) } } @@ -172,11 +174,12 @@ internal class UserIdentity( private val masterKey: CryptoCrossSigningKey, private val selfSigningKey: CryptoCrossSigningKey, private val olmMachine: OlmMachine, - private val requestSender: RequestSender) : UserIdentities() { + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers) : UserIdentities() { /** * The unique ID of the user that this identity belongs to. */ - override fun userId() = this.userId + override fun userId() = userId /** * Manually verify this user identity. @@ -189,8 +192,8 @@ internal class UserIdentity( */ @Throws(SignatureException::class) override suspend fun verify() { - val request = withContext(Dispatchers.Default) { olmMachine.inner().verifyIdentity(userId) } - this.requestSender.sendSignatureUpload(request) + val request = withContext(coroutineDispatchers.computation) { olmMachine.inner().verifyIdentity(userId) } + requestSender.sendSignatureUpload(request) } /** @@ -199,7 +202,7 @@ internal class UserIdentity( * @return True if the identity is considered to be verified and trusted, false otherwise. */ override suspend fun verified(): Boolean { - return withContext(Dispatchers.IO) { olmMachine.inner().isIdentityVerified(userId) } + return withContext(coroutineDispatchers.io) { olmMachine.inner().isIdentityVerified(userId) } } /** @@ -232,32 +235,33 @@ internal class UserIdentity( transactionId: String ): VerificationRequest { val stringMethods = prepareMethods(methods) - val content = this.olmMachine.inner().verificationRequestContent(this.userId, stringMethods)!! + val content = olmMachine.inner().verificationRequestContent(userId, stringMethods)!! - val eventID = requestSender.sendRoomMessage(EventType.MESSAGE, roomId, content, transactionId) + val eventID = requestSender.sendRoomMessage(EventType.MESSAGE, roomId, content, transactionId).eventId - val innerRequest = this.olmMachine.inner().requestVerification(this.userId, roomId, eventID, stringMethods)!! + val innerRequest = olmMachine.inner().requestVerification(userId, roomId, eventID, stringMethods)!! return VerificationRequest( - this.olmMachine.inner(), - innerRequest, - this.requestSender, - this.olmMachine.verificationListeners + machine = olmMachine.inner(), + inner = innerRequest, + sender = requestSender, + coroutineDispatchers = coroutineDispatchers, + listeners = olmMachine.verificationListeners ) } /** * Convert the identity into a MxCrossSigningInfo class. */ - override fun toMxCrossSigningInfo(): MXCrossSigningInfo { -// val crossSigningKeys = listOf(this.masterKey, this.selfSigningKey) - val trustLevel = DeviceTrustLevel(runBlocking { verified() }, false) + override suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo { +// val crossSigningKeys = listOf(masterKey, selfSigningKey) + val trustLevel = DeviceTrustLevel(verified(), false) // TODO remove this, this is silly, we have way too many methods to check if a user is verified masterKey.trustLevel = trustLevel selfSigningKey.trustLevel = trustLevel - return MXCrossSigningInfo(this.userId, listOf( - this.masterKey.also { it.trustLevel = trustLevel }, - this.selfSigningKey.also { it.trustLevel = trustLevel } + return MXCrossSigningInfo(userId, listOf( + masterKey.also { it.trustLevel = trustLevel }, + selfSigningKey.also { it.trustLevel = trustLevel } )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/VerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/VerificationRequest.kt index ebd17307d1..9c1b892bfd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/VerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/VerificationRequest.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.crypto import android.os.Handler import android.os.Looper -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.prepareMethods import timber.log.Timber import uniffi.olm.OlmMachine @@ -45,6 +46,7 @@ internal class VerificationRequest( private val machine: OlmMachine, private var inner: VerificationRequest, private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, private val listeners: ArrayList ) { private val uiHandler = Handler(Looper.getMainLooper()) @@ -53,7 +55,7 @@ internal class VerificationRequest( uiHandler.post { listeners.forEach { try { - it.verificationRequestUpdated(this.toPendingVerificationRequest()) + it.verificationRequestUpdated(toPendingVerificationRequest()) } catch (e: Throwable) { Timber.e(e, "## Error while notifying listeners") } @@ -68,12 +70,12 @@ internal class VerificationRequest( * event that initiated the flow. */ internal fun flowId(): String { - return this.inner.flowId + return inner.flowId } /** The user ID of the other user that is participating in this verification flow */ internal fun otherUser(): String { - return this.inner.otherUserId + return inner.otherUserId } /** The device ID of the other user's device that is participating in this verification flow @@ -83,12 +85,12 @@ internal class VerificationRequest( * */ internal fun otherDeviceId(): String? { refreshData() - return this.inner.otherDeviceId + return inner.otherDeviceId } /** Did we initiate this verification flow */ internal fun weStarted(): Boolean { - return this.inner.weStarted + return inner.weStarted } /** Get the id of the room where this verification is happening @@ -96,7 +98,7 @@ internal class VerificationRequest( * Will be null if the verification is not happening inside a room. */ internal fun roomId(): String? { - return this.inner.roomId + return inner.roomId } /** Did the non-initiating side respond with a m.key.verification.read event @@ -107,13 +109,13 @@ internal class VerificationRequest( */ internal fun isReady(): Boolean { refreshData() - return this.inner.isReady + return inner.isReady } /** Did we advertise that we're able to scan QR codes */ internal fun canScanQrCodes(): Boolean { refreshData() - return this.inner.ourMethods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false + return inner.ourMethods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false } /** Accept the verification request advertising the given methods as supported @@ -132,15 +134,15 @@ internal class VerificationRequest( suspend fun acceptWithMethods(methods: List) { val stringMethods = prepareMethods(methods) - val request = this.machine.acceptVerificationRequest( - this.inner.otherUserId, - this.inner.flowId, + val request = machine.acceptVerificationRequest( + inner.otherUserId, + inner.flowId, stringMethods ) if (request != null) { - this.sender.sendVerificationRequest(request) - this.dispatchRequestUpdated() + sender.sendVerificationRequest(request) + dispatchRequestUpdated() } } @@ -158,12 +160,12 @@ internal class VerificationRequest( * emoji verification, or null if we can't yet transition into emoji verification. */ internal suspend fun startSasVerification(): SasVerification? { - return withContext(Dispatchers.IO) { + return withContext(coroutineDispatchers.io) { val result = machine.startSasVerification(inner.otherUserId, inner.flowId) if (result != null) { sender.sendVerificationRequest(result.request) - SasVerification(machine, result.sas, sender, listeners) + SasVerification(machine, result.sas, sender, coroutineDispatchers, listeners) } else { null } @@ -187,10 +189,10 @@ internal class VerificationRequest( // TODO again, what's the deal with ISO_8859_1? val byteArray = data.toByteArray(Charsets.ISO_8859_1) val encodedData = byteArray.toBase64NoPadding() - val result = this.machine.scanQrCode(this.otherUser(), this.flowId(), encodedData) ?: return null + val result = machine.scanQrCode(otherUser(), flowId(), encodedData) ?: return null - this.sender.sendVerificationRequest(result.request) - return QrCodeVerification(this.machine, this, result.qr, this.sender, this.listeners) + sender.sendVerificationRequest(result.request) + return QrCodeVerification(machine, this, result.qr, sender, coroutineDispatchers, listeners) } /** Transition into a QR code verification to display a QR code @@ -211,15 +213,16 @@ internal class VerificationRequest( * QR code verification, or null if we can't yet transition into QR code verification. */ internal fun startQrVerification(): QrCodeVerification? { - val qrcode = this.machine.startQrVerification(this.inner.otherUserId, this.inner.flowId) + val qrcode = machine.startQrVerification(inner.otherUserId, inner.flowId) return if (qrcode != null) { QrCodeVerification( - this.machine, - this, - qrcode, - this.sender, - this.listeners, + machine = machine, + request = this, + inner = qrcode, + sender = sender, + coroutineDispatchers = coroutineDispatchers, + listeners = listeners, ) } else { null @@ -237,24 +240,24 @@ internal class VerificationRequest( * The method turns into a noop, if the verification flow has already been cancelled. */ internal suspend fun cancel() { - val request = this.machine.cancelVerification( - this.inner.otherUserId, - this.inner.flowId, + val request = machine.cancelVerification( + inner.otherUserId, + inner.flowId, CancelCode.User.value ) if (request != null) { - this.sender.sendVerificationRequest(request) - this.dispatchRequestUpdated() + sender.sendVerificationRequest(request) + dispatchRequestUpdated() } } /** Fetch fresh data from the Rust side for our verification flow */ private fun refreshData() { - val request = this.machine.getVerificationRequest(this.inner.otherUserId, this.inner.flowId) + val request = machine.getVerificationRequest(inner.otherUserId, inner.flowId) if (request != null) { - this.inner = request + inner = request } } @@ -269,7 +272,7 @@ internal class VerificationRequest( */ internal fun toPendingVerificationRequest(): PendingVerificationRequest { refreshData() - val cancelInfo = this.inner.cancelInfo + val cancelInfo = inner.cancelInfo val cancelCode = if (cancelInfo != null) { safeValueOf(cancelInfo.cancelCode) @@ -277,72 +280,72 @@ internal class VerificationRequest( null } - val ourMethods = this.inner.ourMethods - val theirMethods = this.inner.theirMethods - val otherDeviceId = this.inner.otherDeviceId + val ourMethods = inner.ourMethods + val theirMethods = inner.theirMethods + val otherDeviceId = inner.otherDeviceId var requestInfo: ValidVerificationInfoRequest? = null var readyInfo: ValidVerificationInfoReady? = null - if (this.inner.weStarted && ourMethods != null) { + if (inner.weStarted && ourMethods != null) { requestInfo = ValidVerificationInfoRequest( - this.inner.flowId, - this.machine.deviceId(), - ourMethods, - null, + transactionId = inner.flowId, + fromDevice = machine.deviceId(), + methods = ourMethods, + timestamp = null, ) - } else if (!this.inner.weStarted && ourMethods != null) { + } else if (!inner.weStarted && ourMethods != null) { readyInfo = ValidVerificationInfoReady( - this.inner.flowId, - this.machine.deviceId(), - ourMethods, + transactionId = inner.flowId, + fromDevice = machine.deviceId(), + methods = ourMethods, ) } - if (this.inner.weStarted && theirMethods != null && otherDeviceId != null) { + if (inner.weStarted && theirMethods != null && otherDeviceId != null) { readyInfo = ValidVerificationInfoReady( - this.inner.flowId, - otherDeviceId, - theirMethods, + transactionId = inner.flowId, + fromDevice = otherDeviceId, + methods = theirMethods, ) - } else if (!this.inner.weStarted && theirMethods != null && otherDeviceId != null) { + } else if (!inner.weStarted && theirMethods != null && otherDeviceId != null) { requestInfo = ValidVerificationInfoRequest( - this.inner.flowId, - otherDeviceId, - theirMethods, - System.currentTimeMillis(), + transactionId = inner.flowId, + fromDevice = otherDeviceId, + methods = theirMethods, + timestamp = System.currentTimeMillis(), ) } return PendingVerificationRequest( // Creation time - System.currentTimeMillis(), + ageLocalTs = System.currentTimeMillis(), // Who initiated the request - !this.inner.weStarted, + isIncoming = !inner.weStarted, // Local echo id, what to do here? - this.inner.flowId, + localId = inner.flowId, // other user - this.inner.otherUserId, + otherUserId = inner.otherUserId, // room id - this.inner.roomId, + roomId = inner.roomId, // transaction id - this.inner.flowId, + transactionId = inner.flowId, // val requestInfo: ValidVerificationInfoRequest? = null, - requestInfo, + requestInfo = requestInfo, // val readyInfo: ValidVerificationInfoReady? = null, - readyInfo, + readyInfo = readyInfo, // cancel code if there is one - cancelCode, + cancelConclusion = cancelCode, // are we done/successful - this.inner.isDone, + isSuccessful = inner.isDone, // did another device answer the request - this.inner.isPassive, + handledByOtherSession = inner.isPassive, // devices that should receive the events we send out - null, + targetDevices = null ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt index d4864c8234..72aeece896 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt @@ -21,7 +21,6 @@ import android.os.Looper import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -41,7 +40,6 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.MegolmSessionImportManager import org.matrix.android.sdk.internal.crypto.OlmMachineProvider -import org.matrix.android.sdk.internal.crypto.RequestSender import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo @@ -53,6 +51,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.session.SessionScope @@ -95,7 +94,7 @@ internal class RustKeyBackupService @Inject constructor( // private var backupAllGroupSessionsCallback: MatrixCallback? = null - private val importScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val importScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.main) private var keysBackupStateListener: KeysBackupStateListener? = null @@ -237,7 +236,7 @@ internal class RustKeyBackupService @Inject constructor( } } - override fun canRestoreKeys(): Boolean { + override suspend fun canRestoreKeys(): Boolean { val keyCountOnServer = keysBackupVersion?.count ?: return false val keyCountLocally = getTotalNumbersOfKeys() @@ -246,11 +245,11 @@ internal class RustKeyBackupService @Inject constructor( return keyCountLocally < keyCountOnServer } - override fun getTotalNumbersOfKeys(): Int { + override suspend fun getTotalNumbersOfKeys(): Int { return olmMachine.roomKeyCounts().total.toInt() } - override fun getTotalNumbersOfBackedUpKeys(): Int { + override suspend fun getTotalNumbersOfBackedUpKeys(): Int { return olmMachine.roomKeyCounts().backedUp.toInt() } @@ -405,7 +404,7 @@ internal class RustKeyBackupService @Inject constructor( } } - override fun getBackupProgress(progressListener: ProgressListener) { + override suspend fun getBackupProgress(progressListener: ProgressListener) { val backedUpKeys = getTotalNumbersOfBackedUpKeys() val total = getTotalNumbersOfKeys() @@ -490,7 +489,7 @@ internal class RustKeyBackupService @Inject constructor( val data = getKeys(sessionId, roomId, keysVersionResult.version) return withContext(coroutineDispatchers.computation) { - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { stepProgressListener?.onStepProgress(StepProgressListener.Step.DecryptingKey(0, data.roomIdToRoomKeysBackupData.size)) } // Decrypting by chunk of 500 keys in parallel @@ -513,7 +512,7 @@ internal class RustKeyBackupService @Inject constructor( .awaitAll() .flatten() - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { val stepProgress = StepProgressListener.Step.DecryptingKey(data.roomIdToRoomKeysBackupData.size, data.roomIdToRoomKeysBackupData.size) stepProgressListener?.onStepProgress(stepProgress) } @@ -532,7 +531,7 @@ internal class RustKeyBackupService @Inject constructor( val progressListener = if (stepProgressListener != null) { object : ProgressListener { override fun onProgress(progress: Int, total: Int) { - cryptoCoroutineScope.launch(Dispatchers.Main) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { val stepProgress = StepProgressListener.Step.ImportingKey(progress, total) stepProgressListener.onStepProgress(stepProgress) } @@ -581,16 +580,12 @@ internal class RustKeyBackupService @Inject constructor( } override suspend fun getVersion(version: String): KeysVersionResult? { - return withContext(coroutineDispatchers.io) { - sender.getKeyBackupVersion(version) - } + return sender.getKeyBackupVersion(version) } @Throws override suspend fun getCurrentVersion(): KeysVersionResult? { - return withContext(coroutineDispatchers.io) { - sender.getKeyBackupVersion() - } + return sender.getKeyBackupVersion() } override suspend fun forceUsingLastVersion(): Boolean { @@ -646,18 +641,16 @@ internal class RustKeyBackupService @Inject constructor( Timber.w("checkAndStartKeysBackup: invalid state: $state") return@withContext } - keysBackupVersion = null keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver - - withContext(coroutineDispatchers.io) { - try { - val data = getCurrentVersion() - withContext(coroutineDispatchers.crypto) { - checkAndStartWithKeysBackupVersion(data) - } - } catch (failure: Throwable) { - Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + try { + val data = getCurrentVersion() + withContext(coroutineDispatchers.crypto) { + checkAndStartWithKeysBackupVersion(data) + } + } catch (failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + withContext(coroutineDispatchers.crypto) { keysBackupStateManager.state = KeysBackupState.Unknown } } @@ -725,7 +718,7 @@ internal class RustKeyBackupService @Inject constructor( } } - override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + override suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { val info = olmMachine.getBackupKeys() ?: return null return SavedKeyBackupKeyInfo(info.recoveryKey, info.backupVersion) } @@ -878,7 +871,7 @@ internal class RustKeyBackupService @Inject constructor( } } catch (failure: Throwable) { if (failure is Failure.ServerError) { - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { Timber.e(failure, "backupKeys: backupKeys failed.") when (failure.error.code) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt new file mode 100644 index 0000000000..d1ec8e5589 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt @@ -0,0 +1,157 @@ +/* + * 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.network + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider +import org.matrix.android.sdk.internal.crypto.OlmMachine +import timber.log.Timber +import uniffi.olm.Request +import uniffi.olm.RequestType + +private val loggerTag = LoggerTag("OutgoingRequestsProcessor", LoggerTag.CRYPTO) + +internal class OutgoingRequestsProcessor(private val requestSender: RequestSender, + private val coroutineScope: CoroutineScope, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val shieldComputer: ShieldComputer) { + + fun interface ShieldComputer { + suspend fun compute(userIds: List): RoomEncryptionTrustLevel + } + + private val lock: Mutex = Mutex() + + suspend fun process(olmMachine: OlmMachine) { + lock.withLock { + coroutineScope { + Timber.v("OutgoingRequests: ${olmMachine.outgoingRequests()}") + olmMachine.outgoingRequests().map { + when (it) { + is Request.KeysUpload -> { + async { + uploadKeys(olmMachine, it) + } + } + is Request.KeysQuery -> { + async { + queryKeys(olmMachine, it) + } + } + is Request.ToDevice -> { + async { + sendToDevice(olmMachine, it) + } + } + is Request.KeysClaim -> { + async { + claimKeys(olmMachine, it) + } + } + is Request.RoomMessage -> { + async { + sendRoomMessage(olmMachine, it) + } + } + is Request.SignatureUpload -> { + async { + signatureUpload(olmMachine, it) + } + } + is Request.KeysBackup -> { + async { + // The rust-sdk won't ever produce KeysBackup requests here, + // those only get explicitly created. + } + } + } + }.joinAll() + } + } + } + + private suspend fun uploadKeys(olmMachine: OlmMachine, request: Request.KeysUpload) { + try { + val response = requestSender.uploadKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_UPLOAD, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## uploadKeys(): error") + } + } + + private suspend fun queryKeys(olmMachine: OlmMachine, request: Request.KeysQuery) { + try { + val response = requestSender.queryKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response) + coroutineScope.updateShields(request.users) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## queryKeys(): error") + } + } + + private fun CoroutineScope.updateShields(userIds: List) = launch { + cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userIds).forEach { roomId -> + val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId) + val shield = shieldComputer.compute(userGroup) + cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield) + } + } + + private suspend fun sendToDevice(olmMachine: OlmMachine, request: Request.ToDevice) { + try { + requestSender.sendToDevice(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}") + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## sendToDevice(): error") + } + } + + private suspend fun claimKeys(olmMachine: OlmMachine, request: Request.KeysClaim) { + try { + val response = requestSender.claimKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## claimKeys(): error") + } + } + + private suspend fun signatureUpload(olmMachine: OlmMachine, request: Request.SignatureUpload) { + try { + val response = requestSender.sendSignatureUpload(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.SIGNATURE_UPLOAD, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## signatureUpload(): error") + } + } + + private suspend fun sendRoomMessage(olmMachine: OlmMachine, request: Request.RoomMessage) { + try { + val response = requestSender.sendRoomMessage(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.ROOM_MESSAGE, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## sendRoomMessage(): error") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RequestSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RequestSender.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt index 45644d951f..fac164544a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RequestSender.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Matrix.org Foundation C.I.C. + * 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. @@ -14,8 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto +package org.matrix.android.sdk.internal.crypto.network +import com.squareup.moshi.Moshi import com.squareup.moshi.Types import dagger.Lazy import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -46,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask @@ -56,6 +58,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.network.parsing.CheckNumberType +import org.matrix.android.sdk.internal.session.room.send.SendResponse import timber.log.Timber import uniffi.olm.OutgoingVerificationRequest import uniffi.olm.Request @@ -80,6 +83,7 @@ internal class RequestSender @Inject constructor( private val getSessionsDataTask: GetSessionsDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getRoomSessionDataTask: GetRoomSessionDataTask, + private val moshi: Moshi ) { companion object { const val REQUEST_RETRY_COUNT = 3 @@ -97,16 +101,16 @@ internal class RequestSender @Inject constructor( suspend fun queryKeys(request: Request.KeysQuery): String { val params = DownloadKeysForUsersTask.Params(request.users, null) val response = downloadKeysForUsersTask.executeRetry(params, REQUEST_RETRY_COUNT) - val adapter = MoshiProvider.providesMoshi().adapter(KeysQueryResponse::class.java) + val adapter = moshi.adapter(KeysQueryResponse::class.java) return adapter.toJson(response)!! } suspend fun uploadKeys(request: Request.KeysUpload): String { - val body = MoshiProvider.providesMoshi().adapter(Map::class.java).fromJson(request.body)!! + val body = moshi.adapter(Map::class.java).fromJson(request.body)!! val params = UploadKeysTask.Params(body) val response = uploadKeysTask.executeRetry(params, REQUEST_RETRY_COUNT) - val adapter = MoshiProvider.providesMoshi().adapter(KeysUploadResponse::class.java) + val adapter = moshi.adapter(KeysUploadResponse::class.java) return adapter.toJson(response)!! } @@ -118,42 +122,46 @@ internal class RequestSender @Inject constructor( } } - suspend fun sendRoomMessage(request: OutgoingVerificationRequest.InRoom): String { + private suspend fun sendRoomMessage(request: OutgoingVerificationRequest.InRoom): SendResponse { return sendRoomMessage(request.eventType, request.roomId, request.content, request.requestId) } suspend fun sendRoomMessage(request: Request.RoomMessage): String { - return sendRoomMessage(request.eventType, request.roomId, request.content, request.requestId) + val sendResponse = sendRoomMessage(request.eventType, request.roomId, request.content, request.requestId) + val responseAdapter = moshi.adapter(SendResponse::class.java) + return responseAdapter.toJson(sendResponse) } - suspend fun sendRoomMessage(eventType: String, roomId: String, content: String, transactionId: String): String { - val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) - val jsonContent = adapter.fromJson(content) + suspend fun sendRoomMessage(eventType: String, roomId: String, content: String, transactionId: String): SendResponse { + val paramsAdapter = moshi.adapter(Map::class.java) + val jsonContent = paramsAdapter.fromJson(content) val event = Event(eventType, transactionId, jsonContent, roomId = roomId) val params = SendVerificationMessageTask.Params(event) - return this.sendVerificationMessageTask.get().executeRetry(params, REQUEST_RETRY_COUNT) + return sendVerificationMessageTask.get().executeRetry(params, REQUEST_RETRY_COUNT) } - suspend fun sendSignatureUpload(request: Request.SignatureUpload) { - sendSignatureUpload(request.body) + suspend fun sendSignatureUpload(request: Request.SignatureUpload): String { + return sendSignatureUpload(request.body) } - suspend fun sendSignatureUpload(request: SignatureUploadRequest) { - sendSignatureUpload(request.body) + suspend fun sendSignatureUpload(request: SignatureUploadRequest): String { + return sendSignatureUpload(request.body) } - private suspend fun sendSignatureUpload(body: String) { - val adapter = MoshiProvider.providesMoshi().adapter>>(Map::class.java) - val signatures = adapter.fromJson(body)!! + private suspend fun sendSignatureUpload(body: String): String { + val paramsAdapter = moshi.adapter>>(Map::class.java) + val signatures = paramsAdapter.fromJson(body)!! val params = UploadSignaturesTask.Params(signatures) - this.signaturesUploadTask.executeRetry(params, REQUEST_RETRY_COUNT) + val response = signaturesUploadTask.executeRetry(params, REQUEST_RETRY_COUNT) + val responseAdapter = moshi.adapter(SignatureUploadResponse::class.java) + return responseAdapter.toJson(response)!! } suspend fun uploadCrossSigningKeys( request: UploadSigningKeysRequest, interactiveAuthInterceptor: UserInteractiveAuthInterceptor? ) { - val adapter = MoshiProvider.providesMoshi().adapter(RestKeyInfo::class.java) + val adapter = moshi.adapter(RestKeyInfo::class.java) val masterKey = adapter.fromJson(request.masterKey)!!.toCryptoModel() val selfSigningKey = adapter.fromJson(request.selfSigningKey)!!.toCryptoModel() val userSigningKey = adapter.fromJson(request.userSigningKey)!!.toCryptoModel() @@ -195,8 +203,7 @@ internal class RequestSender @Inject constructor( } suspend fun sendToDevice(eventType: String, body: String, transactionId: String) { - val adapter = MoshiProvider - .providesMoshi() + val adapter = moshi .newBuilder() .add(CheckNumberType.JSON_ADAPTER_FACTORY) .build() @@ -252,7 +259,7 @@ internal class RequestSender @Inject constructor( val keys = adapter.fromJson(request.rooms)!! val params = StoreSessionsDataTask.Params(request.version, KeysBackupData(keys)) val response = backupRoomKeysTask.executeRetry(params, REQUEST_RETRY_COUNT) - val responseAdapter = MoshiProvider.providesMoshi().adapter(BackupKeysResult::class.java) + val responseAdapter = moshi.adapter(BackupKeysResult::class.java) return responseAdapter.toJson(response)!! } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 627352f568..259f4cc34b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -22,18 +22,14 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult -import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.awaitCallback import javax.inject.Inject internal interface EncryptEventTask : Task { data class Params(val roomId: String, - val event: Event, - /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ - val keepKeys: List? = null + val event: Event ) } @@ -51,52 +47,34 @@ internal class DefaultEncryptEventTask @Inject constructor( localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING) - val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() - params.keepKeys?.forEach { - localMutableContent.remove(it) - } - -// try { // let it throws - awaitCallback { - cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) - }.let { result -> - val modifiedContent = HashMap(result.eventContent) - params.keepKeys?.forEach { toKeep -> - localEvent.content?.get(toKeep)?.let { - // put it back in the encrypted thing - modifiedContent[toKeep] = it - } - } - val safeResult = result.copy(eventContent = modifiedContent) - // Better handling of local echo, to avoid decrypting transition on remote echo - // Should I only do it for text messages? - val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { - MXEventDecryptionResult( - clearEvent = Event( - type = localEvent.type, - content = localEvent.content, - roomId = localEvent.roomId - ).toContent(), - forwardingCurve25519KeyChain = emptyList(), - senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.getMyDevice().fingerprint() - ) - } else { - null - } - - localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho -> - localEcho.type = EventType.ENCRYPTED - localEcho.content = ContentMapper.map(modifiedContent) - decryptionLocalEcho?.also { - localEcho.setDecryptionResult(it) - } - } - return localEvent.copy( - type = safeResult.eventType, - content = safeResult.eventContent + val result = cryptoService.encryptEventContent(localEvent.content ?: emptyMap(), localEvent.type, params.roomId) + // Better handling of local echo, to avoid decrypting transition on remote echo + // Should I only do it for text messages? + val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + MXEventDecryptionResult( + clearEvent = Event( + type = localEvent.type, + content = localEvent.content, + roomId = localEvent.roomId + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, + claimedEd25519Key = cryptoService.getMyCryptoDevice().fingerprint() ) + } else { + null } + localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho -> + localEcho.type = EventType.ENCRYPTED + localEcho.content = ContentMapper.map(result.eventContent) + decryptionLocalEcho?.also { + localEcho.setDecryptionResult(it) + } + } + return localEvent.copy( + type = result.eventType, + content = result.eventContent + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 6620c1c6cc..33d5e5ab79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -76,8 +76,7 @@ internal class DefaultSendEventTask @Inject constructor( if (params.encrypt && !params.event.isEncrypted()) { return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", - params.event, - listOf("m.relates_to") + params.event )) } return params.event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index c4a6ba27d6..d12b9368f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -22,11 +22,12 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI 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.task.Task import org.matrix.android.sdk.internal.util.toMatrixErrorStr import javax.inject.Inject -internal interface SendVerificationMessageTask : Task { +internal interface SendVerificationMessageTask : Task { data class Params( val event: Event ) @@ -39,10 +40,9 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val globalErrorReceiver: GlobalErrorReceiver) : SendVerificationMessageTask { - override suspend fun execute(params: SendVerificationMessageTask.Params): String { + override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse { val event = handleEncryption(params) val localId = event.eventId!! - try { localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) val response = executeRequest(globalErrorReceiver) { @@ -54,7 +54,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( ) } localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT) - return response.eventId + return response } catch (e: Throwable) { localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr()) throw e @@ -67,7 +67,6 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, - listOf("m.relates_to") )) } catch (throwable: Throwable) { // We said it's ok to send verification request in clear diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt index e03e353cb1..917d1d11c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -15,14 +15,14 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface UploadSignaturesTask : Task { +internal interface UploadSignaturesTask : Task { data class Params( val signatures: Map> ) @@ -33,21 +33,13 @@ internal class DefaultUploadSignaturesTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver ) : UploadSignaturesTask { - override suspend fun execute(params: UploadSignaturesTask.Params) { - try { - val response = executeRequest( - globalErrorReceiver, - canRetry = true, - maxRetriesCount = 10 - ) { - cryptoApi.uploadSignatures(params.signatures) - } - if (response.failures?.isNotEmpty() == true) { - throw Throwable(response.failures.toString()) - } - return - } catch (f: Failure) { - throw f + override suspend fun execute(params: UploadSignaturesTask.Params): SignatureUploadResponse { + return executeRequest( + globalErrorReceiver, + canRetry = true, + maxRetriesCount = 10 + ) { + cryptoApi.uploadSignatures(params.signatures) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt index 262342b054..3e5f200edc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.crypto.verification import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService @@ -26,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.internal.crypto.OlmMachineProvider @@ -77,31 +77,41 @@ internal class RustVerificationService @Inject constructor(private val olmMachin olmMachineProvider.olmMachine } - private val dispatcher = UpdateDispatcher(this.olmMachine.verificationListeners) + private val dispatcher = UpdateDispatcher(olmMachine.verificationListeners) - /** The main entry point for the verification service + /** * * All verification related events should be forwarded through this method to * the verification service. * - * Since 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, this method mainly just + * If the verification event is not encrypted it should be provided to the olmMachine. + * 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. */ - internal suspend fun onEvent(event: Event) = when (event.getClearType()) { - // I'm not entirely sure why getClearType() returns a msgtype in one case - // and a event type in the other case, but this is how the old verification - // service did things and it does seem to work. - MessageType.MSGTYPE_VERIFICATION_REQUEST -> onRequest(event) - EventType.KEY_VERIFICATION_START -> onStart(event) - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE -> onUpdate(event) - else -> { + internal suspend fun onEvent(roomId: String?, event: Event) { + if (roomId != null && !event.isEncrypted()) { + olmMachine.receiveUnencryptedVerificationEvent(roomId, event) + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_REQUEST -> onRequest(event, fromRoomMessage = false) + EventType.KEY_VERIFICATION_START -> onStart(event) + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE -> onUpdate(event) + EventType.MESSAGE -> onRoomMessage(event) + else -> Unit + } + } + + private fun onRoomMessage(event: Event) { + val messageContent = event.getClearContent()?.toModel() ?: return + if (messageContent.msgType == MessageType.MSGTYPE_VERIFICATION_REQUEST) { + onRequest(event, fromRoomMessage = true) } } @@ -110,9 +120,9 @@ internal class RustVerificationService @Inject constructor(private val olmMachin val sender = event.senderId ?: return val flowId = getFlowId(event) ?: return - this.olmMachine.getVerificationRequest(sender, flowId)?.dispatchRequestUpdated() - val verification = this.getExistingTransaction(sender, flowId) ?: return - this.dispatcher.dispatchTxUpdated(verification) + olmMachine.getVerificationRequest(sender, flowId)?.dispatchRequestUpdated() + val verification = getExistingTransaction(sender, flowId) ?: return + dispatcher.dispatchTxUpdated(verification) } /** Check if the start event created new verification objects and dispatch updates */ @@ -120,8 +130,8 @@ internal class RustVerificationService @Inject constructor(private val olmMachin val sender = event.senderId ?: return val flowId = getFlowId(event) ?: return - val verification = this.getExistingTransaction(sender, flowId) ?: return - val request = this.olmMachine.getVerificationRequest(sender, flowId) + val verification = getExistingTransaction(sender, flowId) ?: return + val request = olmMachine.getVerificationRequest(sender, flowId) if (request != null && request.isReady()) { // If this is a SAS verification originating from a `m.key.verification.request` @@ -132,59 +142,54 @@ internal class RustVerificationService @Inject constructor(private val olmMachin Timber.d("## Verification: Auto accepting SAS verification with $sender") verification.accept() } else { - this.dispatcher.dispatchTxUpdated(verification) + dispatcher.dispatchTxUpdated(verification) } } else { // This didn't originate from a request, so tell our listeners that // this is a new verification. - this.dispatcher.dispatchTxAdded(verification) + dispatcher.dispatchTxAdded(verification) // The IncomingVerificationRequestHandler seems to only listen to updates // so let's trigger an update after the addition as well. - this.dispatcher.dispatchTxUpdated(verification) + dispatcher.dispatchTxUpdated(verification) } } /** Check if the request event created a nev verification request object and dispatch that it dis so */ - private fun onRequest(event: Event) { - val flowId = getFlowId(event) ?: return + private fun onRequest(event: Event, fromRoomMessage: Boolean) { + val flowId = if (fromRoomMessage) { + event.eventId + } else { + event.getClearContent().toModel()?.transactionId + } ?: return val sender = event.senderId ?: return + val request = getExistingVerificationRequest(sender, flowId) ?: return - val request = this.getExistingVerificationRequest(sender, flowId) ?: return - - this.dispatcher.dispatchRequestAdded(request) + dispatcher.dispatchRequestAdded(request) } override fun addListener(listener: VerificationService.Listener) { - this.dispatcher.addListener(listener) + dispatcher.addListener(listener) } override fun removeListener(listener: VerificationService.Listener) { - this.dispatcher.removeListener(listener) + dispatcher.removeListener(listener) } - override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - // TODO this doesn't seem to be used anymore? - runBlocking { - val device = olmMachine.getDevice(userId, deviceID) - device?.markAsTrusted() - } - } - - override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { - // TODO This should be handled inside the rust-sdk decryption method + override suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + olmMachine.getDevice(userId, deviceID)?.markAsTrusted() } override fun getExistingTransaction( otherUserId: String, tid: String, ): VerificationTransaction? { - return this.olmMachine.getVerification(otherUserId, tid) + return olmMachine.getVerification(otherUserId, tid) } override fun getExistingVerificationRequests( otherUserId: String ): List { - return this.olmMachine.getVerificationRequests(otherUserId).map { + return olmMachine.getVerificationRequests(otherUserId).map { it.toPendingVerificationRequest() } } @@ -194,7 +199,7 @@ internal class RustVerificationService @Inject constructor(private val olmMachin tid: String? ): PendingVerificationRequest? { return if (tid != null) { - this.olmMachine.getVerificationRequest(otherUserId, tid)?.toPendingVerificationRequest() + olmMachine.getVerificationRequest(otherUserId, tid)?.toPendingVerificationRequest() } else { null } @@ -224,29 +229,24 @@ internal class RustVerificationService @Inject constructor(private val olmMachin return null } - override fun requestKeyVerification( - methods: List, - otherUserId: String, - otherDevices: List? - ): PendingVerificationRequest { - val verification = when (val identity = runBlocking { olmMachine.getIdentity(otherUserId) }) { - is OwnUserIdentity -> runBlocking { identity.requestVerification(methods) } + override suspend fun requestSelfKeyVerification(methods: List): PendingVerificationRequest { + val verification = when (val identity = olmMachine.getIdentity(olmMachine.userId())) { + is OwnUserIdentity -> identity.requestVerification(methods) is UserIdentity -> throw IllegalArgumentException("This method doesn't support verification of other users devices") null -> throw IllegalArgumentException("Cross signing has not been bootstrapped for our own user") } - return verification.toPendingVerificationRequest() } - override fun requestKeyVerificationInDMs( + override suspend fun requestKeyVerificationInDMs( methods: List, otherUserId: String, roomId: String, localId: String? ): PendingVerificationRequest { - Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") - val verification = when (val identity = runBlocking { olmMachine.getIdentity(otherUserId) }) { - is UserIdentity -> runBlocking { identity.requestVerification(methods, roomId, localId!!) } + olmMachine.ensureUsersKeys(listOf(otherUserId)) + val verification = when (val identity = olmMachine.getIdentity(otherUserId)) { + is UserIdentity -> identity.requestVerification(methods, roomId, localId!!) 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") } @@ -254,21 +254,20 @@ internal class RustVerificationService @Inject constructor(private val olmMachin return verification.toPendingVerificationRequest() } - override fun readyPendingVerification( + override suspend fun readyPendingVerification( methods: List, otherUserId: String, transactionId: String ): Boolean { - val request = this.olmMachine.getVerificationRequest(otherUserId, transactionId) - + val request = olmMachine.getVerificationRequest(otherUserId, transactionId) return if (request != null) { - runBlocking { request.acceptWithMethods(methods) } + request.acceptWithMethods(methods) if (request.isReady()) { val qrcode = request.startQrVerification() if (qrcode != null) { - this.dispatcher.dispatchTxAdded(qrcode) + dispatcher.dispatchTxAdded(qrcode) } true @@ -280,82 +279,50 @@ internal class RustVerificationService @Inject constructor(private val olmMachin } } - override fun readyPendingVerificationInDMs( - methods: List, - otherUserId: String, - roomId: String, - transactionId: String - ): Boolean { - return readyPendingVerification(methods, otherUserId, transactionId) - } - - override fun beginKeyVerification( + override suspend fun beginKeyVerification( method: VerificationMethod, otherUserId: String, - otherDeviceId: String, - transactionId: String? + transactionId: String ): String? { return if (method == VerificationMethod.SAS) { - if (transactionId != null) { - val request = this.olmMachine.getVerificationRequest(otherUserId, transactionId) + val request = olmMachine.getVerificationRequest(otherUserId, transactionId) - runBlocking { - val sas = request?.startSasVerification() + val sas = request?.startSasVerification() - if (sas != null) { - dispatcher.dispatchTxAdded(sas) - sas.transactionId - } else { - null - } - } + if (sas != null) { + dispatcher.dispatchTxAdded(sas) + sas.transactionId } else { - // This starts the short SAS flow, the one that doesn't start with - // a `m.key.verification.request`, Element web stopped doing this, might - // be wise do do so as well - // DeviceListBottomSheetViewModel triggers this, interestingly the method that - // triggers this is called `manuallyVerify()` - runBlocking { - val verification = olmMachine.getDevice(otherUserId, otherDeviceId)?.startVerification() - if (verification != null) { - dispatcher.dispatchTxAdded(verification) - verification.transactionId - } else { - null - } - } + null } } else { throw IllegalArgumentException("Unknown verification method") } } - override fun beginKeyVerificationInDMs( - method: VerificationMethod, - transactionId: String, - roomId: String, - otherUserId: String, - otherDeviceId: String - ): String { - beginKeyVerification(method, otherUserId, otherDeviceId, transactionId) - // TODO what's the point of returning the same ID we got as an argument? - // We do this because the old verification service did so - return transactionId - } - - override fun cancelVerificationRequest(request: PendingVerificationRequest) { - val verificationRequest = request.transactionId?.let { - this.olmMachine.getVerificationRequest(request.otherUserId, it) + override suspend fun beginDeviceVerification(otherUserId: String, otherDeviceId: String): String? { + // This starts the short SAS flow, the one that doesn't start with + // a `m.key.verification.request`, Element web stopped doing this, might + // be wise do do so as well + // DeviceListBottomSheetViewModel triggers this, interestingly the method that + // triggers this is called `manuallyVerify()` + val otherDevice = olmMachine.getDevice(otherUserId, otherDeviceId) + val verification = otherDevice?.startVerification() + return if (verification != null) { + dispatcher.dispatchTxAdded(verification) + verification.transactionId + } else { + null } - runBlocking { verificationRequest?.cancel() } } - override fun declineVerificationRequestInDMs( - otherUserId: String, - transactionId: String, - roomId: String - ) { - val verificationRequest = this.olmMachine.getVerificationRequest(otherUserId, transactionId) - runBlocking { verificationRequest?.cancel() } + override suspend fun cancelVerificationRequest(request: PendingVerificationRequest) { + request.transactionId ?: return + cancelVerificationRequest(request.otherUserId, request.transactionId) + } + + override suspend fun cancelVerificationRequest(otherUserId: String, transactionId: String) { + val verificationRequest = olmMachine.getVerificationRequest(otherUserId, transactionId) + verificationRequest?.cancel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt deleted file mode 100644 index 858c0ab6af..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 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.qrcode - -import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding -import java.security.SecureRandom - -fun generateSharedSecretV2(): String { - val secureRandom = SecureRandom() - - // 8 bytes long - val secretBytes = ByteArray(8) - secureRandom.nextBytes(secretBytes) - return secretBytes.toBase64NoPadding() -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 1e533158a7..6327b28d0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -177,7 +177,6 @@ internal class DefaultSession @Inject constructor( assert(!isOpen) isOpen = true globalErrorHandler.listener = this - cryptoService.get().ensureDevice() uiHandler.post { lifecycleObservers.forEach { it.onSessionStarted(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 2d8c3e9c78..a0c3ac23b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask import org.matrix.android.sdk.internal.session.space.DefaultSpace -import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException internal class DefaultRoom(override val roomId: String, @@ -117,9 +116,7 @@ internal class DefaultRoom(override val roomId: String, } override suspend fun prepareToEncrypt() { - awaitCallback { - cryptoService.prepareToEncrypt(roomId, it) - } + cryptoService.prepareToEncrypt(roomId) } override suspend fun enableEncryption(algorithm: String, force: Boolean) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index e0d501c515..cd06d47f05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -156,7 +156,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( * Invoke the event decryption mechanism for a specific event */ - private fun decryptIfNeeded(event: Event, roomId: String) { + private suspend fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index b7a2cf2fce..9e1b33037f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -43,6 +43,7 @@ internal class RealmSendingEventsDataSource( private var roomEntity: RoomEntity? = null private var sendingTimelineEvents: RealmList? = null private var frozenSendingTimelineEvents: RealmList? = null + private val builtEvents = ArrayList() private val sendingTimelineEventsListener = RealmChangeListener> { events -> uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 8d62c6e8ec..7c52aa1b9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.Realm import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event @@ -126,7 +127,9 @@ internal class TimelineEventDecryptor @Inject constructor( return } try { - val result = cryptoService.decryptEvent(request.event, timelineId) + val result = runBlocking { + cryptoService.decryptEvent(request.event, timelineId) + } Timber.v("Successfully decrypted event ${event.eventId}") realm.executeTransaction { val eventId = event.eventId ?: return@executeTransaction diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8be342c6d1..7b5c08ebd6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import dagger.Lazy import io.realm.Realm import io.realm.kotlin.createObject +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -343,7 +344,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return roomEntity } - private suspend fun handleTimelineEvents(realm: Realm, + private fun handleTimelineEvents(realm: Realm, roomId: String, roomEntity: RoomEntity, eventList: List, @@ -458,7 +459,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun decryptIfNeeded(event: Event, roomId: String) { try { // Event from sync does not have roomId, so add it to the event first - val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + val result = runBlocking { + cryptoService.decryptEvent(event.copy(roomId = roomId), "") + } event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index c82b543a08..b1d6b8e158 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -134,10 +134,8 @@ class VerifySessionInteractiveTest : VerificationTestBase() { onView(withId(R.id.bottomSheetFragmentContainer)) .check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session))))) - val request = existingSession!!.cryptoService().verificationService().requestKeyVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - existingSession!!.myUserId, - listOf(uiSession.sessionParams.deviceId!!) + val request = existingSession!!.cryptoService().verificationService().requestSelfKeyVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) ) val transactionId = request.transactionId!! diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index 87ed51522f..17e2949cda 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -64,12 +64,12 @@ fun Session.startSyncing(context: Context) { /** * Tell is the session has unsaved e2e keys in the backup */ -fun Session.hasUnsavedKeys(): Boolean { +suspend fun Session.hasUnsavedKeys(): Boolean { return cryptoService().inboundGroupSessionsCount(false) > 0 && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp } -fun Session.cannotLogoutSafely(): Boolean { +suspend fun Session.cannotLogoutSafely(): Boolean { // has some encrypted chat return hasUnsavedKeys() || // has local cross signing keys diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt index 438b502b42..2bf16e9971 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingViewState.kt @@ -26,5 +26,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR data class KeysBackupSettingViewState(val keysBackupVersionTrust: Async = Uninitialized, val keysBackupState: KeysBackupState? = null, val keysBackupVersion: KeysVersionResult? = null, + val remainingKeysToBackup: Int = 0, val deleteBackupRequest: Async = Uninitialized) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 577572ef14..c177cf5da8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -124,10 +124,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor( style(ItemStyle.BIG_TEXT) hasIndeterminateProcess(true) - val totalKeys = host.session.cryptoService().inboundGroupSessionsCount(false) - val backedUpKeys = host.session.cryptoService().inboundGroupSessionsCount(true) - - val remainingKeysToBackup = totalKeys - backedUpKeys + val remainingKeysToBackup = data.remainingKeysToBackup if (data.keysBackupVersionTrust()?.usable == false) { description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence()) diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index 51213fcd38..8ca14e562a 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -18,7 +18,6 @@ package im.vector.app.features.crypto.keysbackup.settings import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import timber.log.Timber class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState, session: Session @@ -46,6 +44,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + private val cryptoService = session.cryptoService() private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService() init { @@ -75,34 +74,12 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS private fun getKeysBackupTrust() = withState { state -> val versionResult = keysBackupService.keysBackupVersion - Timber.d("BACKUP: HEEEEEEE $versionResult ${state.keysBackupVersionTrust}") - if (state.keysBackupVersionTrust is Uninitialized && versionResult != null) { - setState { - copy( - keysBackupVersionTrust = Loading(), - deleteBackupRequest = Uninitialized - ) - } - Timber.d("BACKUP: HEEEEEEE TWO") - - viewModelScope.launch { - try { - val data = keysBackupService.getKeysBackupTrust(versionResult) - Timber.d("BACKUP: HEEEE suceeeded $data") - setState { - copy( - keysBackupVersionTrust = Success(data) - ) - } - } catch (failure: Throwable) { - Timber.d("BACKUP: HEEEE FAILED $failure") - setState { - copy( - keysBackupVersionTrust = Fail(failure) - ) - } - } + setState { copy(deleteBackupRequest = Uninitialized) } + suspend { + keysBackupService.getKeysBackupTrust(versionResult) + }.execute { + copy(keysBackupVersionTrust = it) } } } @@ -119,10 +96,24 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS keysBackupVersion = keysBackupService.keysBackupVersion ) } - + when (newState) { + KeysBackupState.BackingUp, KeysBackupState.WillBackUp -> updateKeysCount() + else -> Unit + } getKeysBackupTrust() } + private fun updateKeysCount() { + viewModelScope.launch { + val totalKeys = cryptoService.inboundGroupSessionsCount(false) + val backedUpKeys = cryptoService.inboundGroupSessionsCount(true) + val remainingKeysToBackup = totalKeys - backedUpKeys + setState { + copy(remainingKeysToBackup = remainingKeysToBackup) + } + } + } + private fun deleteCurrentBackup() { val keysBackupService = keysBackupService diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt index 949037bc3c..274ac4a144 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt @@ -128,7 +128,7 @@ class KeyRequestHandler @Inject constructor( } if (deviceInfo.isUnknown) { - session?.cryptoService()?.setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), userId, deviceId) + session?.cryptoService()?.verificationService()?.markedLocallyAsManuallyVerified(userId, deviceId) deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 8994ad901b..7197385deb 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -149,10 +149,12 @@ class SharedSecureStorageViewModel @AssistedInject constructor( // as we are going to reset, we'd better cancel all outgoing requests // if not they could be accepted in the middle of the reset process // and cause strange use cases - session.cryptoService().verificationService().getExistingVerificationRequests(session.myUserId).forEach { - session.cryptoService().verificationService().cancelVerificationRequest(it) + viewModelScope.launch { + session.cryptoService().verificationService().getExistingVerificationRequests(session.myUserId).forEach { + session.cryptoService().verificationService().cancelVerificationRequest(it) + } + _viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet) } - _viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet) } private fun handleResetAll() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index de627e1f78..23cd54ed99 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -34,7 +34,6 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -91,12 +90,7 @@ class BootstrapCrossSigningTask @Inject constructor( ) try { - awaitCallback { - crossSigningService.initializeCrossSigning( - params.userInteractiveAuthInterceptor, - it - ) - } + crossSigningService.initializeCrossSigning(params.userInteractiveAuthInterceptor) if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 3a3f1054f1..6b01403fe3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -24,6 +24,8 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationService @@ -42,7 +44,8 @@ import javax.inject.Singleton class IncomingVerificationRequestHandler @Inject constructor( private val context: Context, private var avatarRenderer: Provider, - private val popupAlertManager: PopupAlertManager) : VerificationService.Listener { + private val popupAlertManager: PopupAlertManager, + private val coroutineScope: CoroutineScope) : VerificationService.Listener { private var session: Session? = null @@ -61,7 +64,7 @@ class IncomingVerificationRequestHandler @Inject constructor( // TODO maybe check also if val uid = "kvr_${tx.transactionId}" when (tx.state) { - is VerificationTxState.OnStarted -> { + is VerificationTxState.OnStarted -> { // Add a notification for every incoming request val user = session?.getUser(tx.otherUserId) val name = user?.toMatrixItem()?.getBestName() ?: tx.otherUserId @@ -88,12 +91,14 @@ class IncomingVerificationRequestHandler @Inject constructor( it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId) } } - dismissedAction = Runnable { + dismissedAction = LaunchCoroutineRunnable(coroutineScope) { tx.cancel() } addButton( context.getString(R.string.action_ignore), - { tx.cancel() } + LaunchCoroutineRunnable(coroutineScope) { + tx.cancel() + } ) addButton( context.getString(R.string.action_open), @@ -160,10 +165,9 @@ class IncomingVerificationRequestHandler @Inject constructor( } } } - dismissedAction = Runnable { - session?.cryptoService()?.verificationService()?.declineVerificationRequestInDMs(pr.otherUserId, - pr.transactionId ?: "", - pr.roomId ?: "" + dismissedAction = LaunchCoroutineRunnable(coroutineScope) { + session?.cryptoService()?.verificationService()?.cancelVerificationRequest(pr.otherUserId, + pr.transactionId ?: "" ) } colorAttribute = R.attr.vctr_notice_secondary @@ -181,6 +185,14 @@ class IncomingVerificationRequestHandler @Inject constructor( } } + private class LaunchCoroutineRunnable(private val coroutineScope: CoroutineScope, private val block: suspend () -> Unit) : Runnable { + override fun run() { + coroutineScope.launch { + block() + } + } + } + private fun uniqueIdForVerificationRequest(pr: PendingVerificationRequest) = "verificationRequest_${pr.transactionId}" } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index ee3bbe8f31..446f2f61d1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -16,8 +16,6 @@ package im.vector.app.features.crypto.verification import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success @@ -131,34 +129,37 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( session.cryptoService().verificationService().getExistingTransaction(initialState.otherUserId, it) as? QrCodeVerificationTransaction } - val hasAnyOtherSession = session.cryptoService() - .getCryptoDeviceInfo(session.myUserId) - .any { - it.deviceId != session.sessionParams.deviceId - } + viewModelScope.launch { - setState { - copy( - otherUserMxItem = userItem?.toMatrixItem(), - sasTransactionState = sasTx?.state, - qrTransactionState = qrTx?.state, - transactionId = pr?.transactionId ?: initialState.verificationId, - pendingRequest = if (pr != null) Success(pr) else Uninitialized, - isMe = initialState.otherUserId == session.myUserId, - currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), - quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup(), - hasAnyOtherSession = hasAnyOtherSession - ) - } + val hasAnyOtherSession = session.cryptoService() + .getCryptoDeviceInfoList(session.myUserId) + .any { + it.deviceId != session.sessionParams.deviceId + } - if (autoReady) { - // TODO, can I be here in DM mode? in this case should test if roomID is null? - session.cryptoService().verificationService() - .readyPendingVerification( - supportedVerificationMethodsProvider.provide(), - pr!!.otherUserId, - pr.transactionId ?: "" - ) + setState { + copy( + otherUserMxItem = userItem?.toMatrixItem(), + sasTransactionState = sasTx?.state, + qrTransactionState = qrTx?.state, + transactionId = pr?.transactionId ?: initialState.verificationId, + pendingRequest = if (pr != null) Success(pr) else Uninitialized, + isMe = initialState.otherUserId == session.myUserId, + currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), + quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup(), + hasAnyOtherSession = hasAnyOtherSession + ) + } + + if (autoReady) { + // TODO, can I be here in DM mode? in this case should test if roomID is null? + session.cryptoService().verificationService() + .readyPendingVerification( + supportedVerificationMethodsProvider.provide(), + pr!!.otherUserId, + pr.transactionId ?: "" + ) + } } } @@ -192,14 +193,16 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun cancelAllPendingVerifications(state: VerificationBottomSheetViewState) { - session.cryptoService() - .verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let { - session.cryptoService().verificationService().cancelVerificationRequest(it) - } - session.cryptoService() - .verificationService() - .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") - ?.cancel(CancelCode.User) + viewModelScope.launch { + session.cryptoService() + .verificationService().getExistingVerificationRequest(state.otherUserMxItem?.id ?: "", state.transactionId)?.let { + session.cryptoService().verificationService().cancelVerificationRequest(it) + } + session.cryptoService() + .verificationService() + .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") + ?.cancel(CancelCode.User) + } } fun continueFromCancel() { @@ -232,109 +235,25 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( when (action) { is VerificationAction.RequestVerificationByDM -> { - if (roomId == null) { - val localId = LocalEcho.createLocalEchoId() - setState { - copy( - pendingLocalId = localId, - pendingRequest = Loading() - ) - } - viewModelScope.launch { - val result = runCatching { session.createDirectRoom(otherUserId) } - result.fold( - { data -> - setState { - copy( - roomId = data, - pendingRequest = Success( - session - .cryptoService() - .verificationService() - .requestKeyVerificationInDMs( - supportedVerificationMethodsProvider.provide(), - otherUserId, - data, - pendingLocalId - ) - ) - ) - } - }, - { failure -> - setState { - copy(pendingRequest = Fail(failure)) - } - } - ) - } - } else { - setState { - copy( - pendingRequest = Success(session - .cryptoService() - .verificationService() - .requestKeyVerificationInDMs(supportedVerificationMethodsProvider.provide(), otherUserId, roomId) - ) - ) - } - } - Unit + handleRequestVerificationByDM(roomId, otherUserId) } is VerificationAction.StartSASVerification -> { - val request = session.cryptoService().verificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId) - ?: return@withState - val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice - if (roomId == null) { - session.cryptoService().verificationService().beginKeyVerification( - VerificationMethod.SAS, - otherUserId = request.otherUserId, - otherDeviceId = otherDevice ?: "", - transactionId = action.pendingRequestTransactionId - ) - } else { - session.cryptoService().verificationService().beginKeyVerificationInDMs( - VerificationMethod.SAS, - transactionId = action.pendingRequestTransactionId, - roomId = roomId, - otherUserId = request.otherUserId, - otherDeviceId = otherDevice ?: "" - ) - } - Unit + handleStartSASVerification(otherUserId, action) } is VerificationAction.RemoteQrCodeScanned -> { - val existingTransaction = session.cryptoService().verificationService() - .getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction - existingTransaction - ?.userHasScannedOtherQrCode(action.scannedData) + handleRemoteQrCodeScanned(action) } is VerificationAction.OtherUserScannedSuccessfully -> { - val transactionId = state.transactionId ?: return@withState - - val existingTransaction = session.cryptoService().verificationService() - .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction - existingTransaction - ?.otherUserScannedMyQrCode() + handleOtherUserScannedSuccessfully(state.transactionId, otherUserId) } is VerificationAction.OtherUserDidNotScanned -> { - val transactionId = state.transactionId ?: return@withState - - val existingTransaction = session.cryptoService().verificationService() - .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction - existingTransaction - ?.otherUserDidNotScannedMyQrCode() + handleOtherUserDidNotScanned(state.transactionId, otherUserId) } is VerificationAction.SASMatchAction -> { - (session.cryptoService().verificationService() - .getExistingTransaction(action.otherUserId, action.sasTransactionId) - as? SasVerificationTransaction)?.userHasVerifiedShortCode() + handleSASMatchAction(action) } is VerificationAction.SASDoNotMatchAction -> { - (session.cryptoService().verificationService() - .getExistingTransaction(action.otherUserId, action.sasTransactionId) - as? SasVerificationTransaction) - ?.shortCodeDoesNotMatch() + handleSASDoNotMatchAction(action) } is VerificationAction.GotItConclusion -> { _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) @@ -365,6 +284,85 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( }.exhaustive } + private fun handleStartSASVerification(otherUserId: String, action: VerificationAction.StartSASVerification) { + val request = session.cryptoService().verificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId) + ?: return + viewModelScope.launch { + session.cryptoService().verificationService().beginKeyVerification( + VerificationMethod.SAS, + otherUserId = request.otherUserId, + transactionId = action.pendingRequestTransactionId + ) + } + } + + private fun handleSASDoNotMatchAction(action: VerificationAction.SASDoNotMatchAction) { + viewModelScope.launch { + (session.cryptoService().verificationService() + .getExistingTransaction(action.otherUserId, action.sasTransactionId) + as? SasVerificationTransaction) + ?.shortCodeDoesNotMatch() + } + } + + private fun handleSASMatchAction(action: VerificationAction.SASMatchAction) { + viewModelScope.launch { + (session.cryptoService().verificationService() + .getExistingTransaction(action.otherUserId, action.sasTransactionId) + as? SasVerificationTransaction)?.userHasVerifiedShortCode() + } + } + + private fun handleOtherUserDidNotScanned(transactionId: String?, otherUserId: String) { + transactionId ?: return + viewModelScope.launch { + val existingTransaction = session.cryptoService().verificationService() + .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction + existingTransaction + ?.otherUserDidNotScannedMyQrCode() + } + } + + private fun handleOtherUserScannedSuccessfully(transactionId: String?, otherUserId: String) { + transactionId ?: return + viewModelScope.launch { + val existingTransaction = session.cryptoService().verificationService() + .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction + existingTransaction + ?.otherUserScannedMyQrCode() + } + } + + private fun handleRemoteQrCodeScanned(action: VerificationAction.RemoteQrCodeScanned) { + viewModelScope.launch { + val existingTransaction = session.cryptoService().verificationService() + .getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction + existingTransaction + ?.userHasScannedOtherQrCode(action.scannedData) + } + } + + private fun handleRequestVerificationByDM(roomId: String?, otherUserId: String) { + viewModelScope.launch { + val localId = LocalEcho.createLocalEchoId() + val dmRoomId = roomId ?: session.createDirectRoom(otherUserId) + setState { copy(pendingLocalId = localId, roomId = dmRoomId) } + suspend { + session + .cryptoService() + .verificationService() + .requestKeyVerificationInDMs( + supportedVerificationMethodsProvider.provide(), + otherUserId, + dmRoomId, + localId + ) + }.execute { + copy(pendingRequest = it, roomId = dmRoomId) + } + } + } + private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) { viewModelScope.launch(Dispatchers.IO) { try { @@ -446,60 +444,66 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } + private fun handleTransactionUpdate(state: VerificationBottomSheetViewState, tx: VerificationTransaction) { + viewModelScope.launch { + if (state.selfVerificationMode && state.transactionId == null) { + // is this an incoming with that user + if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) { + // Also auto accept incoming if needed! + // TODO is state.transactionId ever null for self verifications, doesn't seem + // like this will ever trigger + if (tx is SasVerificationTransaction && tx.state == VerificationTxState.OnStarted) { + tx.acceptVerification() + } + /* + if (tx is IncomingSasVerificationTransaction) { + if (tx.uxState == IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + tx.performAccept() + } + } + */ + // Use this one! + setState { + copy( + transactionId = tx.transactionId, + sasTransactionState = tx.state.takeIf { tx is SasVerificationTransaction }, + qrTransactionState = tx.state.takeIf { tx is QrCodeVerificationTransaction } + ) + } + } + } + + when (tx) { + is SasVerificationTransaction -> { + if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) { + // A SAS tx has been started following this request + setState { + copy( + sasTransactionState = tx.state + ) + } + } + } + is QrCodeVerificationTransaction -> { + if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) { + // A QR tx has been started following this request + setState { + copy( + qrTransactionState = tx.state + ) + } + } + } + } + } + } + override fun transactionCreated(tx: VerificationTransaction) { transactionUpdated(tx) } override fun transactionUpdated(tx: VerificationTransaction) = withState { state -> - if (state.selfVerificationMode && state.transactionId == null) { - // is this an incoming with that user - if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) { - // Also auto accept incoming if needed! - // TODO is state.transactionId ever null for self verifications, doesn't seem - // like this will ever trigger - if (tx is SasVerificationTransaction && tx.state == VerificationTxState.OnStarted) { - tx.acceptVerification() - } - /* - if (tx is IncomingSasVerificationTransaction) { - if (tx.uxState == IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - tx.performAccept() - } - } - */ - // Use this one! - setState { - copy( - transactionId = tx.transactionId, - sasTransactionState = tx.state.takeIf { tx is SasVerificationTransaction }, - qrTransactionState = tx.state.takeIf { tx is QrCodeVerificationTransaction } - ) - } - } - } - - when (tx) { - is SasVerificationTransaction -> { - if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) { - // A SAS tx has been started following this request - setState { - copy( - sasTransactionState = tx.state - ) - } - } - } - is QrCodeVerificationTransaction -> { - if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) { - // A QR tx has been started following this request - setState { - copy( - qrTransactionState = tx.state - ) - } - } - } - } + handleTransactionUpdate(state, tx) } override fun verificationRequestCreated(pr: PendingVerificationRequest) { @@ -514,12 +518,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( if (!pr.isReady) { // auto ready in this case, as we are waiting // TODO, can I be here in DM mode? in this case should test if roomID is null? - session.cryptoService().verificationService() - .readyPendingVerification( - supportedVerificationMethodsProvider.provide(), - pr.otherUserId, - pr.transactionId ?: "" - ) + viewModelScope.launch { + session.cryptoService().verificationService() + .readyPendingVerification( + supportedVerificationMethodsProvider.provide(), + pr.otherUserId, + pr.transactionId ?: "" + ) + } } // Use this one! diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index ca8aeaba85..9d93e2261f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -220,30 +219,27 @@ class HomeActivityViewModel @AssistedInject constructor( // Try to initialize cross signing in background if possible Timber.d("Initialize cross signing...") try { - awaitCallback { - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - // We missed server grace period or it's not setup, see if we remember locally password - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && - errCode == null && - reAuthHelper.data != null) { - promise.resume( - UserPasswordAuth( - session = flowResponse.session, - user = session.myUserId, - password = reAuthHelper.data - ) - ) - } else { - promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) - } + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + // We missed server grace period or it's not setup, see if we remember locally password + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + errCode == null && + reAuthHelper.data != null) { + promise.resume( + UserPasswordAuth( + session = flowResponse.session, + user = session.myUserId, + password = reAuthHelper.data + ) + ) + } else { + promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) } - }, - callback = it - ) - Timber.d("Initialize cross signing SUCCESS") - } + } + }, + ) + Timber.d("Initialize cross signing SUCCESS") } catch (failure: Throwable) { Timber.e(failure, "Failed to initialize cross signing") } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index 1aee0257f4..c7d44221f6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -80,6 +80,7 @@ class HomeDrawerFragment @Inject constructor( } // Sign out views.homeDrawerHeaderSignoutView.debouncedClicks { + signout() sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) SignOutUiWorker(requireActivity()).perform() } @@ -118,4 +119,7 @@ class HomeDrawerFragment @Inject constructor( navigator.openDebug(requireActivity()) } } + + private fun signout() { + } } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 8a36a4c19e..1abb25fb20 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -32,10 +32,11 @@ import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample -import org.matrix.android.sdk.api.NoOpMatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.MatrixItem @@ -58,7 +59,7 @@ data class DeviceDetectionInfo( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, session: Session, private val vectorPreferences: VectorPreferences) : - VectorViewModel(initialState) { + VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() @@ -75,12 +76,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted init { - val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) - .firstOrNull { it.deviceId == session.sessionParams.deviceId } - ?.firstTimeSeenLocalTs - ?: System.currentTimeMillis() - Timber.v("## Detector - Current Session first time seen $currentSessionTs") - ignoredDeviceList.addAll( vectorPreferences.getUnknownDeviceDismissedList().also { Timber.v("## Detector - Remembered ignored list $it") @@ -90,10 +85,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo(), - session.flow().liveCrossSigningPrivateKeys() - ) { cryptoList, infoList, pInfo -> + session.flow().liveCrossSigningPrivateKeys(), + session.firstTimeDeviceSeen(), + ) { cryptoList, infoList, pInfo, firstTimeDeviceSeen -> // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") + Timber.v("## Detector - Current Session first time seen $firstTimeDeviceSeen") infoList .filter { info -> // filter verified session, by checking the crypto device info @@ -106,7 +103,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 DeviceDetectionInfo( deviceInfo, - deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + deviceKnownSince > firstTimeDeviceSeen + 60_000, // short window to avoid false positive, pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change ) } @@ -125,12 +122,14 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted .sample(5_000) .onEach { // If we have a new crypto device change, we might want to trigger refresh of device info - session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + session.cryptoService().fetchDevicesList() } .launchIn(viewModelScope) // trigger a refresh of lastSeen / last Ip - session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + viewModelScope.launch { + session.cryptoService().fetchDevicesList() + } } override fun handle(action: Action) { @@ -154,4 +153,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) super.onCleared() } + + private fun Session.firstTimeDeviceSeen() = flow { + val value = cryptoService().getCryptoDeviceInfoList(myUserId) + .firstOrNull { it.deviceId == sessionParams.deviceId } + ?.firstTimeSeenLocalTs + ?: System.currentTimeMillis() + emit(value) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 3bdcbc6529..ea89a1c017 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -994,22 +994,24 @@ class TimelineViewModel @AssistedInject constructor( private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}") - if (session.cryptoService().verificationService().readyPendingVerificationInDMs( - supportedVerificationMethodsProvider.provide(), - action.otherUserId, - room.roomId, - action.transactionId)) { - _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) - } else { - // TODO + viewModelScope.launch { + if (session.cryptoService().verificationService().readyPendingVerification( + supportedVerificationMethodsProvider.provide(), + action.otherUserId, + action.transactionId)) { + _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + } else { + // TODO + } } } private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { - session.cryptoService().verificationService().declineVerificationRequestInDMs( - action.otherUserId, - action.transactionId, - room.roomId) + viewModelScope.launch { + session.cryptoService().verificationService().cancelVerificationRequest( + action.otherUserId, + action.transactionId) + } } private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 5575d9b7f6..46777f63f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -164,11 +164,12 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted onEach(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> val nonNullTimelineEvent = timelineEvent() ?: return@onEach eventIdFlow.tryEmit(nonNullTimelineEvent.eventId) + val events = actionsForEvent(nonNullTimelineEvent, permissions) setState { copy( eventId = nonNullTimelineEvent.eventId, messageBody = computeMessageBody(nonNullTimelineEvent), - actions = actionsForEvent(nonNullTimelineEvent, permissions) + actions = events ) } } @@ -246,7 +247,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List { + private suspend fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List { val messageContent = timelineEvent.getLastMessageContent() val msgType = messageContent?.msgType @@ -317,7 +318,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted // TODO sent by me or sufficient power level } - private fun ArrayList.addActionsForSyncedState(timelineEvent: TimelineEvent, + private suspend fun ArrayList.addActionsForSyncedState(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions, messageContent: MessageContent?, msgType: String?) { @@ -400,7 +401,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted ) { add(EventSharedAction.UseKeyBackup) } - if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1 || + if (session.cryptoService().getCryptoDeviceInfoList(session.myUserId).size > 1 || timelineEvent.senderInfo.userId != session.myUserId) { add(EventSharedAction.ReRequestKey(timelineEvent.eventId)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 59b39d17ef..e5fef0c980 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -27,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -138,53 +139,55 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } } - private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { - return if ( - event.root.sendState == SendState.SYNCED && - roomSummary?.isEncrypted.orFalse() && - // is user verified - session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { - val ts = roomSummary?.encryptionEventTs ?: 0 - val eventTs = event.root.originServerTs ?: 0 - if (event.isEncrypted()) { - // Do not decorate failed to decrypt, or redaction (we lost sender device info) - if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { - E2EDecoration.NONE - } else { - val sendingDevice = event.root.content - .toModel() - ?.deviceId - ?.let { deviceId -> - session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) - } - when { - sendingDevice == null -> { - // For now do not decorate this with warning - // maybe it's a deleted session - E2EDecoration.NONE - } - sendingDevice.trustLevel == null -> { - E2EDecoration.WARN_SENT_BY_UNKNOWN - } - sendingDevice.trustLevel?.isVerified().orFalse() -> { - E2EDecoration.NONE - } - else -> { - E2EDecoration.WARN_SENT_BY_UNVERIFIED - } - } - } + private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration = runBlocking { + if (event.root.sendState != SendState.SYNCED) { + return@runBlocking E2EDecoration.NONE + } + if (!roomSummary?.isEncrypted.orFalse()) { + return@runBlocking E2EDecoration.NONE + } + val isUserVerified = session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted().orFalse() + if (!isUserVerified) { + return@runBlocking E2EDecoration.NONE + } + val ts = roomSummary?.encryptionEventTs ?: 0 + val eventTs = event.root.originServerTs ?: 0 + return@runBlocking if (event.isEncrypted()) { + // Do not decorate failed to decrypt, or redaction (we lost sender device info) + if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + E2EDecoration.NONE } else { - if (event.root.isStateEvent()) { - // Do not warn for state event, they are always in clear - E2EDecoration.NONE - } else { - // If event is in clear after the room enabled encryption we should warn - if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + val sendingDevice = event.root.content + .toModel() + ?.deviceId + ?.let { deviceId -> + session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId) + } + when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session + E2EDecoration.NONE + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN + } + sendingDevice.trustLevel?.isVerified().orFalse() -> { + E2EDecoration.NONE + } + else -> { + E2EDecoration.WARN_SENT_BY_UNVERIFIED + } } } } else { - E2EDecoration.NONE + if (event.root.isStateEvent()) { + // Do not warn for state event, they are always in clear + E2EDecoration.NONE + } else { + // If event is in clear after the room enabled encryption we should warn + if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index fb2f3b8301..e7c1193858 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -100,6 +100,8 @@ import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgsBuilder import im.vector.app.space +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom @@ -116,7 +118,8 @@ class DefaultNavigator @Inject constructor( private val widgetArgsBuilder: WidgetArgsBuilder, private val appStateHandler: AppStateHandler, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, - private val features: VectorFeatures + private val features: VectorFeatures, + private val coroutineScope: CoroutineScope ) : Navigator { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { @@ -193,55 +196,57 @@ class DefaultNavigator @Inject constructor( } override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { - val session = sessionHolder.getSafeActiveSession() ?: return - val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) - ?: return - if (tx is SasVerificationTransaction && tx.isIncoming) { - tx.acceptVerification() - } + coroutineScope.launch { + val session = sessionHolder.getSafeActiveSession() ?: return@launch + val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) + ?: return@launch + if (tx is SasVerificationTransaction && tx.isIncoming) { + tx.acceptVerification() + } - if (context is AppCompatActivity) { - VerificationBottomSheet.withArgs( - roomId = null, - otherUserId = otherUserId, - transactionId = sasTransactionId - ).show(context.supportFragmentManager, "REQPOP") + if (context is AppCompatActivity) { + VerificationBottomSheet.withArgs( + roomId = null, + otherUserId = otherUserId, + transactionId = sasTransactionId + ).show(context.supportFragmentManager, "REQPOP") + } } } override fun requestSessionVerification(context: Context, otherSessionId: String) { - val session = sessionHolder.getSafeActiveSession() ?: return - val pr = session.cryptoService().verificationService().requestKeyVerification( - supportedVerificationMethodsProvider.provide(), - session.myUserId, - listOf(otherSessionId) - ) - if (context is AppCompatActivity) { - VerificationBottomSheet.withArgs( - roomId = null, - otherUserId = session.myUserId, - transactionId = pr.transactionId - ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) + coroutineScope.launch { + val session = sessionHolder.getSafeActiveSession() ?: return@launch + val pr = session.cryptoService().verificationService().requestSelfKeyVerification( + supportedVerificationMethodsProvider.provide() + ) + if (context is AppCompatActivity) { + VerificationBottomSheet.withArgs( + roomId = null, + otherUserId = session.myUserId, + transactionId = pr.transactionId + ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) + } } } override fun requestSelfSessionVerification(context: Context) { - val session = sessionHolder.getSafeActiveSession() ?: return - val otherSessions = session.cryptoService() - .getCryptoDeviceInfo(session.myUserId) - .filter { it.deviceId != session.sessionParams.deviceId } - .map { it.deviceId } - if (context is AppCompatActivity) { - if (otherSessions.isNotEmpty()) { - val pr = session.cryptoService().verificationService().requestKeyVerification( - supportedVerificationMethodsProvider.provide(), - session.myUserId, - otherSessions) - VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId) - .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) - } else { - VerificationBottomSheet.forSelfVerification(session) - .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) + coroutineScope.launch { + val session = sessionHolder.getSafeActiveSession() ?: return@launch + val otherSessions = session.cryptoService() + .getCryptoDeviceInfoList(session.myUserId) + .filter { it.deviceId != session.sessionParams.deviceId } + .map { it.deviceId } + if (context is AppCompatActivity) { + if (otherSessions.isNotEmpty()) { + val pr = session.cryptoService().verificationService().requestSelfKeyVerification( + supportedVerificationMethodsProvider.provide()) + VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId) + .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) + } else { + VerificationBottomSheet.forSelfVerification(session) + .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) + } } } } @@ -404,13 +409,15 @@ class DefaultNavigator @Inject constructor( override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { // if cross signing is enabled and trusted or not set up at all we should propose full 4S sessionHolder.getSafeActiveSession()?.let { session -> - if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null || - session.cryptoService().crossSigningService().canCrossSign()) { - (context as? AppCompatActivity)?.let { - BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) + coroutineScope.launch { + if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null || + session.cryptoService().crossSigningService().canCrossSign()) { + (context as? AppCompatActivity)?.let { + BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) + } + } else { + context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } - } else { - context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index ec034173fc..3cdc9e8c76 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -190,7 +190,7 @@ class NotifiableEventResolver @Inject constructor( } } - private fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { + private suspend fun TimelineEvent.attemptToDecryptIfNeeded(session: Session) { if (root.isEncrypted() && root.mxDecryptionResult == null) { // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index d2491237ca..2c6daae6b6 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -30,9 +30,9 @@ import im.vector.app.core.di.SingletonEntryPoint import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow @@ -124,8 +124,13 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) { if (!initialState.allowDeviceAction) return - session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, initialState.userId, action.deviceId, null)?.let { txID -> - _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(initialState.userId, txID)) + viewModelScope.launch { + session.cryptoService().verificationService().beginDeviceVerification( + otherUserId = initialState.userId, + otherDeviceId = action.deviceId, + )?.let { txID -> + _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(initialState.userId, txID)) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 0bbdd87f3e..f1a8173d53 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.roomprofile.members -import androidx.lifecycle.asFlow import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -94,8 +93,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState if (room.isEncrypted()) { room.flow().liveRoomMembers(roomMemberQueryParams) .flatMapLatest { membersSummary -> - session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) - .asFlow() + session.cryptoService().getLiveCryptoDeviceInfoList(membersSummary.map { it.userId }) .catch { Timber.e(it) } .map { deviceList -> // If any key change, emit the userIds list diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index ef87d908ea..b7c04ac559 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -70,12 +70,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import me.gujun.android.span.span -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( @@ -317,31 +315,32 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // Todo this should be refactored and use same state as 4S section private fun refreshXSigningStatus() { - val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() - val xSigningIsEnableInAccount = crossSigningKeys != null - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + lifecycleScope.launchWhenResumed { + val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - when { - xSigningKeyCanSign -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) - } - xSigningKeysAreTrusted -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) - } - xSigningIsEnableInAccount -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) - } - else -> { - mCrossSigningStatePreference.setIcon(android.R.color.transparent) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + when { + xSigningKeyCanSign -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) + } + xSigningKeysAreTrusted -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) + } + xSigningIsEnableInAccount -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) + } + else -> { + mCrossSigningStatePreference.setIcon(android.R.color.transparent) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + } } + mCrossSigningStatePreference.isVisible = true } - - mCrossSigningStatePreference.isVisible = true } private val saveMegolmStartForActivityResult = registerStartForActivityResult { @@ -523,7 +522,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( /** * Build the cryptography preference section. */ - private fun refreshCryptographyPreference(devices: List) { + private suspend fun refreshCryptographyPreference(devices: List) { showDeviceListPref.isEnabled = devices.isNotEmpty() showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size) @@ -553,7 +552,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } // crypto section: device key (fingerprint) - val deviceInfo = session.cryptoService().getDeviceInfo(userId, deviceId) + val deviceInfo = session.cryptoService().getCryptoDeviceInfo(userId, deviceId) val fingerprint = deviceInfo?.fingerprint() if (fingerprint?.isNotEmpty() == true) { @@ -579,28 +578,19 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // ============================================================================================================== private fun refreshMyDevice() { - session.cryptoService().getUserDevices(session.myUserId).map { - DeviceInfo( - userId = session.myUserId, - deviceId = it.deviceId, - displayName = it.displayName() - ) - }.let { - refreshCryptographyPreference(it) + viewLifecycleOwner.lifecycleScope.launchWhenResumed { + session.cryptoService().getUserDevices(session.myUserId).map { + DeviceInfo( + userId = session.myUserId, + deviceId = it.deviceId, + displayName = it.displayName() + ) + }.let { + refreshCryptographyPreference(it) + } + // TODO Move to a ViewModel... + val devicesList = session.cryptoService().fetchDevicesList() + refreshCryptographyPreference(devicesList) } - // TODO Move to a ViewModel... - session.cryptoService().fetchDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - if (isAdded) { - refreshCryptographyPreference(data.devices.orEmpty()) - } - } - - override fun onFailure(failure: Throwable) { - if (isAdded) { - refreshCryptographyPreference(emptyList()) - } - } - }) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 644b7f33dd..270cc93cc2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -41,7 +42,6 @@ import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -55,25 +55,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { init { - combine( - session.flow().liveMyDevicesInfo(), - session.flow().liveCrossSigningInfo(session.myUserId) - ) { myDevicesInfo, mxCrossSigningInfo -> - myDevicesInfo to mxCrossSigningInfo - } - .execute { data -> - val crossSigningKeys = data.invoke()?.second?.getOrNull() - val xSigningIsEnableInAccount = crossSigningKeys != null - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - - copy( - crossSigningInfo = crossSigningKeys, - xSigningIsEnableInAccount = xSigningIsEnableInAccount, - xSigningKeysAreTrusted = xSigningKeysAreTrusted, - xSigningKeyCanSign = xSigningKeyCanSign - ) - } + observeCrossSigning() } var uiaContinuation: Continuation? = null @@ -90,29 +72,27 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) viewModelScope.launch(Dispatchers.IO) { try { - awaitCallback { - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, - errCode: String?, - promise: Continuation) { - Timber.d("## UIA : initializeCrossSigning UIA") - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && - reAuthHelper.data != null && errCode == null) { - UserPasswordAuth( - session = null, - user = session.myUserId, - password = reAuthHelper.data - ).let { promise.resume(it) } - } else { - Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") - _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise - } + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation) { + Timber.d("## UIA : initializeCrossSigning UIA") + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") + _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise } - }, it) - } + } + }) } catch (failure: Throwable) { handleInitializeXSigningError(failure) } finally { @@ -149,6 +129,28 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( }.exhaustive } + private fun observeCrossSigning() { + combine( + session.flow().liveUserCryptoDevices(session.myUserId), + session.flow().liveCrossSigningInfo(session.myUserId) + ) { myDevicesInfo, mxCrossSigningInfo -> + myDevicesInfo to mxCrossSigningInfo + }.onEach { data -> + val crossSigningKeys = data.second.getOrNull() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + setState { + copy( + crossSigningInfo = crossSigningKeys, + xSigningIsEnableInAccount = xSigningIsEnableInAccount, + xSigningKeysAreTrusted = xSigningKeysAreTrusted, + xSigningKeyCanSign = xSigningKeyCanSign + ) + } + } + } + private fun handleInitializeXSigningError(failure: Throwable) { Timber.e(failure, "## CrossSigning - Failed to initialize cross signing") _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing)))) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index 3a944b5a71..937445e815 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -15,7 +15,6 @@ */ package im.vector.app.features.settings.devices -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +25,7 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -43,14 +43,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As by hiltMavericksViewModelFactory() init { - - setState { - copy( - hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(), - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), - isRecoverySetup = session.sharedSecretStorageService.isRecoverySetup() - ) - } + initState() session.flow().liveCrossSigningInfo(session.myUserId) .execute { copy( @@ -78,10 +71,6 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As ) } - setState { - copy(deviceInfo = Loading()) - } - session.flow().liveMyDevicesInfo() .map { devices -> devices.firstOrNull { it.deviceId == initialState.deviceId } ?: DeviceInfo(deviceId = initialState.deviceId) @@ -91,6 +80,21 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As } } + private fun initState() { + viewModelScope.launch { + val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized() + val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + val isRecoverySetup = session.sharedSecretStorageService.isRecoverySetup() + setState { + copy( + hasAccountCrossSigning = hasAccountCrossSigning, + accountCrossSigningIsTrusted = accountCrossSigningIsTrusted, + isRecoverySetup = isRecoverySetup + ) + } + } + } + override fun handle(action: EmptyAction) { } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index ab871e8b18..17d91f893c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -35,7 +35,6 @@ import im.vector.app.core.utils.PublishDataSource import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import im.vector.lib.core.utils.flow.throttleFirst -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -43,8 +42,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth @@ -53,17 +50,14 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation @@ -106,15 +100,7 @@ class DevicesViewModel @AssistedInject constructor( private val refreshSource = PublishDataSource() init { - - setState { - copy( - hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(), - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), - myDeviceId = session.sessionParams.deviceId ?: "" - ) - } - + initState() combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() @@ -155,7 +141,7 @@ class DevicesViewModel @AssistedInject constructor( .sample(5_000) .onEach { // If we have a new crypto device change, we might want to trigger refresh of device info - session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + session.cryptoService().fetchDevicesList() } .launchIn(viewModelScope) @@ -168,7 +154,7 @@ class DevicesViewModel @AssistedInject constructor( refreshSource.stream().throttleFirst(4_000) .onEach { - session.cryptoService().fetchDevicesList(NoOpMatrixCallback()) + session.cryptoService().fetchDevicesList() session.cryptoService().downloadKeys(listOf(session.myUserId), true) } .launchIn(viewModelScope) @@ -176,6 +162,21 @@ class DevicesViewModel @AssistedInject constructor( queryRefreshDevicesList() } + private fun initState() { + viewModelScope.launch { + val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized() + val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + val myDeviceId = session.sessionParams.deviceId ?: "" + setState { + copy( + hasAccountCrossSigning = hasAccountCrossSigning, + accountCrossSigningIsTrusted = accountCrossSigningIsTrusted, + myDeviceId = myDeviceId + ) + } + } + } + override fun onCleared() { session.cryptoService().verificationService().removeListener(this) super.onCleared() @@ -240,13 +241,15 @@ class DevicesViewModel @AssistedInject constructor( } private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) { - val txID = session.cryptoService() - .verificationService() - .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null) - _viewEvents.post(DevicesViewEvents.ShowVerifyDevice( - session.myUserId, - txID - )) + viewModelScope.launch { + val txID = session.cryptoService() + .verificationService() + .beginDeviceVerification(session.myUserId, action.deviceId) + _viewEvents.post(DevicesViewEvents.ShowVerifyDevice( + session.myUserId, + txID + )) + } } private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state -> @@ -268,8 +271,7 @@ class DevicesViewModel @AssistedInject constructor( } } else { // legacy - session.cryptoService().setDeviceVerification( - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + session.cryptoService().verificationService().markedLocallyAsManuallyVerified( action.cryptoDeviceInfo.userId, action.cryptoDeviceInfo.deviceId) } @@ -288,27 +290,21 @@ class DevicesViewModel @AssistedInject constructor( } private fun handleRename(action: DevicesAction.Rename) { - session.cryptoService().setDeviceName(action.deviceId, action.newName, object : MatrixCallback { - override fun onSuccess(data: Unit) { + viewModelScope.launch { + try { + session.cryptoService().setDeviceName(action.deviceId, action.newName) setState { - copy( - request = Success(data) - ) + copy(request = Success(Unit)) } // force settings update queryRefreshDevicesList() - } - - override fun onFailure(failure: Throwable) { + } catch (failure: Throwable) { setState { - copy( - request = Fail(failure) - ) + copy(request = Fail(failure)) } - _viewEvents.post(DevicesViewEvents.Failure(failure)) } - }) + } } /** @@ -323,39 +319,32 @@ class DevicesViewModel @AssistedInject constructor( ) } - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { try { - awaitCallback { - session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - Timber.d("## UIA : deleteDevice UIA") - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { - UserPasswordAuth( - session = null, - user = session.myUserId, - password = reAuthHelper.data - ).let { promise.resume(it) } - } else { - Timber.d("## UIA : deleteDevice UIA > start reauth activity") - _viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise - } + session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + Timber.d("## UIA : deleteDevice UIA") + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : deleteDevice UIA > start reauth activity") + _viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise } - }, it) - } + } + }) setState { - copy( - request = Success(Unit) - ) + copy(request = Success(Unit)) } - // force settings update queryRefreshDevicesList() } catch (failure: Throwable) { setState { - copy( - request = Fail(failure) - ) + copy(request = Fail(failure)) } if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error)))) diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt index 9acc81d0c2..bf445a7c34 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity.kt @@ -95,7 +95,7 @@ class SoftLogoutActivity : LoginActivity() { MainActivity.restartApp(this, MainActivityArgs()) } - views.loginLoading.isVisible = softLogoutViewState.isLoading() + views.loginLoading.isVisible = softLogoutViewState.isLoading } companion object { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 0cd9cde547..74ce1ef615 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -34,6 +34,7 @@ import im.vector.app.features.signout.soft.epoxy.loginRedButtonItem import im.vector.app.features.signout.soft.epoxy.loginTextItem import im.vector.app.features.signout.soft.epoxy.loginTitleItem import im.vector.app.features.signout.soft.epoxy.loginTitleSmallItem +import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject class SoftLogoutController @Inject constructor( @@ -52,6 +53,7 @@ class SoftLogoutController @Inject constructor( override fun buildModels() { val safeViewState = viewState ?: return + if (safeViewState.hasUnsavedKeys is Incomplete) return buildHeader(safeViewState) buildForm(safeViewState) @@ -78,7 +80,7 @@ class SoftLogoutController @Inject constructor( state.userDisplayName, state.userId)) } - if (state.hasUnsavedKeys) { + if (state.hasUnsavedKeys().orFalse()) { loginTextItem { id("signText2") text(host.stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice)) diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index f40f35a6e2..44696f9505 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.features.login.AbstractLoginFragment import im.vector.app.features.login.LoginAction import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginViewEvents +import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject /** @@ -118,7 +119,7 @@ class SoftLogoutFragment @Inject constructor( withState(softLogoutViewModel) { state -> cleanupUi() - val messageResId = if (state.hasUnsavedKeys) { + val messageResId = if (state.hasUnsavedKeys().orFalse()) { R.string.soft_logout_clear_data_dialog_e2e_warning_content } else { R.string.soft_logout_clear_data_dialog_content diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 00422d8872..7bc88c2b7e 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -69,7 +69,6 @@ class SoftLogoutViewModel @AssistedInject constructor( userId = userId, deviceId = session.sessionParams.deviceId.orEmpty(), userDisplayName = session.getUser(userId)?.displayName ?: userId, - hasUnsavedKeys = session.hasUnsavedKeys() ) } else { SoftLogoutViewState( @@ -77,17 +76,25 @@ class SoftLogoutViewModel @AssistedInject constructor( userId = "", deviceId = "", userDisplayName = "", - hasUnsavedKeys = false ) } } } init { + checkHasUnsavedKeys() // Get the supported login flow getSupportedLoginFlow() } + private fun checkHasUnsavedKeys() { + suspend { + session.hasUnsavedKeys() + }.execute { + copy(hasUnsavedKeys = it) + } + } + private fun getSupportedLoginFlow() { viewModelScope.launch { authenticationService.cancelPendingLoginOrRegistration() diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt index 511711ab2f..2bed442fe8 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewState.kt @@ -30,13 +30,12 @@ data class SoftLogoutViewState( val userId: String, val deviceId: String, val userDisplayName: String, - val hasUnsavedKeys: Boolean, + val hasUnsavedKeys: Async = Uninitialized, val enteredPassword: String = "" ) : MavericksState { - fun isLoading(): Boolean { - return asyncLoginAction is Loading || + val isLoading: Boolean = + asyncLoginAction is Loading || // Keep loading when it is success because of the delay to switch to the next Activity asyncLoginAction is Success - } } diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index ded42e23e0..c35363d66f 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -130,14 +130,14 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS /** * Safe way to get the number of keys to backup */ - fun getNumberOfKeysToBackup(): Int { + private suspend fun getNumberOfKeysToBackup(): Int { return session.cryptoService().inboundGroupSessionsCount(false) } /** * Safe way to tell if there are more keys on the server */ - fun canRestoreKeys(): Boolean { + private suspend fun canRestoreKeys(): Boolean { return session.cryptoService().keysBackupService().canRestoreKeys() } @@ -161,5 +161,5 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS } } - override fun handle(action: EmptyAction) {} + override fun handle(action: EmptyAction) = Unit } diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt index 29c094bff4..66b69fd53c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutUiWorker.kt @@ -17,17 +17,25 @@ package im.vector.app.features.workers.signout import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.cannotLogoutSafely import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session class SignOutUiWorker(private val activity: FragmentActivity) { fun perform() { val session = activity.singletonEntryPoint().activeSessionHolder().getSafeActiveSession() ?: return + activity.lifecycleScope.perform(session) + } + + private fun CoroutineScope.perform(session: Session) = launch { if (session.cannotLogoutSafely()) { // The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready val signOutDialog = SignOutBottomSheetDialogFragment.newInstance() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 1ec1f31b45..625c0675c1 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -30,9 +30,9 @@ class FakeCryptoService : CryptoService by mockk() { override fun getLiveCryptoDeviceInfo() = MutableLiveData(cryptoDeviceInfos.values.toList()) - override fun getLiveCryptoDeviceInfo(userId: String) = getLiveCryptoDeviceInfo(listOf(userId)) + override fun getLiveCryptoDeviceInfoList(userId: String) = getLiveCryptoDeviceInfo(listOf(userId)) - override fun getLiveCryptoDeviceInfo(userIds: List) = MutableLiveData( + override fun getLiveCryptoDeviceInfoList(userIds: List) = MutableLiveData( cryptoDeviceInfos.filterKeys { userIds.contains(it) }.values.toList() ) }