diff --git a/dependencies.gradle b/dependencies.gradle index 9641a63f263b3ac5cb21a96cd3cef9625299beb3..3bf3ab746d8753ec420b11dd0cd6cd741679f9d4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,36 +1,37 @@ ext.versions = [ 'minSdk' : 21, - 'compileSdk' : 31, - 'targetSdk' : 31, + 'compileSdk' : 32, + 'targetSdk' : 32, 'sourceCompat' : JavaVersion.VERSION_11, 'targetCompat' : JavaVersion.VERSION_11, ] - -// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142 -// Please test carefully before upgrading again. -def gradle = "7.1.3" +def gradle = "7.2.2" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.6.21" +def kotlin = "1.7.20" def kotlinCoroutines = "1.6.4" -def dagger = "2.42" +def dagger = "2.44" def appDistribution = "16.0.0-beta04" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" -def moshi = "1.13.0" +def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" def flipper = "0.164.0" def epoxy = "4.6.2" def mavericks = "2.7.0" -def glide = "4.13.2" +def glide = "4.14.1" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" -def vanniktechEmoji = "0.15.0" +// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert +// the whole commit which set version 0.16.0-SNAPSHOT +def vanniktechEmoji = "0.16.0-SNAPSHOT" + +def sentry = "6.4.1" -def fragment = "1.5.2" +def fragment = "1.5.3" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 @@ -51,7 +52,7 @@ ext.libs = [ ], androidx : [ 'activity' : "androidx.activity:activity:1.5.1", - 'appCompat' : "androidx.appcompat:appcompat:1.4.2", + 'appCompat' : "androidx.appcompat:appcompat:1.5.1", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.8.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", @@ -86,7 +87,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.55" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.56" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -100,7 +101,7 @@ ext.libs = [ 'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper", ], element : [ - 'opusencoder' : "io.element.android:opusencoder:1.0.4", + 'opusencoder' : "io.element.android:opusencoder:1.1.0", ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -120,6 +121,7 @@ ext.libs = [ markwon : [ 'core' : "io.noties.markwon:core:$markwon", 'extLatex' : "io.noties.markwon:ext-latex:$markwon", + 'imageGlide' : "io.noties.markwon:image-glide:$markwon", 'inlineParser' : "io.noties.markwon:inline-parser:$markwon", 'html' : "io.noties.markwon:html:$markwon" ], @@ -165,10 +167,13 @@ ext.libs = [ apache : [ 'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator" ], + sentry: [ + 'sentryAndroid' : "io.sentry:sentry-android:$sentry" + ], tests : [ 'kluent' : "org.amshove.kluent:kluent-android:1.68", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", - 'junit' : "junit:junit:4.13.2" + 'junit' : "junit:junit:4.13.2", ] ] diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 433bc5356829ae66e491a3e2ea6b46698715dbe1..cdab6172d1068cf9c58980673baae54fadab9b9a 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -38,10 +38,18 @@ ext.groups = [ 'com.google.testing.platform', ] ], + snapshot: [ + regex: [ + ], + group: [ + 'com.vanniktech', + ] + ], mavenCentral: [ regex: [ ], group: [ + 'app.cash.paparazzi', 'ch.qos.logback', 'com.adevinta.android', 'com.airbnb.android', @@ -118,7 +126,7 @@ ext.groups = [ 'com.sun.xml.bind.mvn', 'com.sun.xml.fastinfoset', 'com.thoughtworks.qdox', - 'com.vanniktech', + // 'com.vanniktech', 'commons-cli', 'commons-codec', 'commons-io', @@ -140,14 +148,18 @@ ext.groups = [ 'io.opencensus', 'io.reactivex.rxjava2', 'io.realm', + 'io.sentry', 'it.unimi.dsi', 'jakarta.activation', 'jakarta.xml.bind', + 'javax.activation', 'javax.annotation', 'javax.inject', + 'javax.xml.bind', 'jline', 'jp.wasabeef', 'junit', + 'kxml2', 'me.saket', 'net.bytebuddy', 'net.java', @@ -176,11 +188,13 @@ ext.groups = [ 'org.hamcrest', 'org.jacoco', 'org.java-websocket', + 'org.jcodec', 'org.jetbrains', 'org.jetbrains.dokka', 'org.jetbrains.intellij.deps', 'org.jetbrains.kotlin', 'org.jetbrains.kotlinx', + 'org.jetbrains.trove4j', 'org.json', 'org.jsoup', 'org.junit', @@ -197,7 +211,6 @@ ext.groups = [ 'org.ow2.asm', 'org.ow2.asm', 'org.reactivestreams', - 'org.robolectric', 'org.slf4j', 'org.sonatype.oss', 'org.testng', diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 9dbc5f42a3f6ef8680b89c35e8dbcec4d426091e..9836e1c8bb285132b6c9b3e298fbba3afef654a5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -224,6 +224,8 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.androidx.coreTesting androidTestImplementation libs.jetbrains.coroutinesAndroid + androidTestImplementation libs.jetbrains.coroutinesTest + // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index 260e8dbe058bb7d73ee87d1409fcd61cfa8cde8b..403f697778e28864e5cf38cd4d08edaa58af116f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) // Change password - commonTestHelper.runBlockingTest { - session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) - } + session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) // Try to login with the previous password, it will fail val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 0b21f85742d513bfac201c648958a4990e8b099f..bb5618b81633ae560e2b2cd9228cd477e3d2cba7 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -40,26 +40,24 @@ import kotlin.coroutines.resume class DeactivateAccountTest : InstrumentedTest { @Test - fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper -> + fun deactivateAccountTest() = runSessionTest(context(), autoSignoutOnClose = false /* session will be deactivated */) { commonTestHelper -> val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Deactivate the account - commonTestHelper.runBlockingTest { - session.accountService().deactivateAccount( - eraseAllData = false, - userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { - promise.resume( - UserPasswordAuth( - user = session.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } + session.accountService().deactivateAccount( + eraseAllData = false, + userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume( + UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) } - ) - } + } + ) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) @@ -74,23 +72,19 @@ class DeactivateAccountTest : InstrumentedTest { // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) val hs = commonTestHelper.createHomeServerConfig() - commonTestHelper.runBlockingTest { - commonTestHelper.matrix.authenticationService.getLoginFlow(hs) - } + commonTestHelper.matrix.authenticationService.getLoginFlow(hs) var accountCreationError: Throwable? = null - commonTestHelper.runBlockingTest { - try { - commonTestHelper.matrix.authenticationService - .getRegistrationWizard() - .createAccount( - session.myUserId.substringAfter("@").substringBefore(":"), - TestConstants.PASSWORD, - null - ) - } catch (failure: Throwable) { - accountCreationError = failure - } + try { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount( + session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null + ) + } catch (failure: Throwable) { + accountCreationError = failure } // Test the error 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 43f42a3ed460c08d009a1d5d67ea846aee584bf8..eeb2def5827876275de455c482a8090c7b32d6d6 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 @@ -19,23 +19,22 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri import android.util.Log -import androidx.lifecycle.Observer import androidx.test.internal.runner.junit4.statement.UiThreadStatement -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.SyncConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.crypto.MXCryptoConfig @@ -51,12 +50,12 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.session.sync.SyncState import timber.log.Timber import java.util.UUID -import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * This class exposes methods to be used in common cases @@ -65,34 +64,42 @@ import java.util.concurrent.TimeUnit class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) { companion object { - internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { - val testHelper = CommonTestHelper(context) - return try { - block(testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context, cryptoConfig) + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } - internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, - cryptoConfig: MXCryptoConfig? = null, - block: (CryptoTestHelper, CommonTestHelper) -> Unit) { + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { val testHelper = CommonTestHelper(context, cryptoConfig) val cryptoTestHelper = CryptoTestHelper(testHelper) - return try { - block(cryptoTestHelper, testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(cryptoTestHelper, testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } } internal val matrix: TestMatrix - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var accountNumber = 0 private val trackedSessions = mutableListOf<Session>() @@ -107,6 +114,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MatrixConfiguration( applicationFlavor = "TestFlavor", roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), + syncConfig = SyncConfig(longPollTimeout = 5_000L), cryptoConfig = cryptoConfig ?: MXCryptoConfig() ) ) @@ -114,19 +122,17 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: matrix = _matrix!! } - fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) } - fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + suspend fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { return logIntoAccount(userId, TestConstants.PASSWORD, testParams) } - fun cleanUpOpenedSessions() { + suspend fun cleanUpOpenedSessions() { trackedSessions.forEach { - runBlockingTest { - it.signOutService().signOut(true) - } + it.signOutService().signOut(true) } trackedSessions.clear() } @@ -140,27 +146,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: .build() } - /** - * This methods init the event stream and check for initial sync - * - * @param session the session to sync - */ - fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { - val lock = CountDownLatch(1) - coroutineScope.launch { - session.syncService().startSync(true) - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer<SyncState> { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - lock.countDown() - syncLiveData.removeObserver(this) - } - } - } - syncLiveData.observeForever(syncObserver) - } - await(lock, timeout) + suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { + session.syncService().startSync(true) + val syncLiveData = session.syncService().getSyncStateLive() + syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() } } /** @@ -168,22 +157,11 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: * * @param session the session to sync */ - fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { - waitWithLatch(timeout) { latch -> - session.clearCache() - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer<SyncState> { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - Timber.v("Clear cache and synced") - syncLiveData.removeObserver(this) - latch.countDown() - } - } - } - syncLiveData.observeForever(syncObserver) - session.syncService().startSync(true) - } + suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { + session.clearCache() + syncSession(session, timeout) + session.syncService().getSyncStateLive().first(timeout) { session.syncService().hasAlreadySynced() } + Timber.v("Clear cache and synced") } /** @@ -193,7 +171,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ - fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> { + suspend fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> { val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) timeline.start() val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) @@ -206,66 +184,72 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: /** * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> { + private suspend fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> { val sentEvents = ArrayList<TimelineEvent>(count) (1 until count + 1) .map { "$message #$it" } .chunked(10) .forEach { batchedMessages -> - batchedMessages.forEach { formattedMessage -> - if (rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = rootThreadEventId, - replyInThreadText = formattedMessage - ) - } else { - room.sendService().sendTextMessage(formattedMessage) - } - } - waitWithLatch(timeout) { latch -> - val timelineListener = object : Timeline.Listener { - - override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { - val allSentMessages = snapshot - .filter { it.root.sendState == SendState.SYNCED } - .filter { it.root.getClearType() == EventType.MESSAGE } - .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true } - - val hasSyncedAllBatchedMessages = allSentMessages - .map { - it.root.getClearContent().toModel<MessageContent>()?.body + waitFor( + continueWhen = { + wrapWithTimeout(timeout) { + suspendCoroutine<Unit> { continuation -> + val timelineListener = object : Timeline.Listener { + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + val allSentMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true } + + val hasSyncedAllBatchedMessages = allSentMessages + .map { + it.root.getClearContent().toModel<MessageContent>()?.body + } + .containsAll(batchedMessages) + + if (allSentMessages.size == count) { + sentEvents.addAll(allSentMessages) + } + if (hasSyncedAllBatchedMessages) { + timeline.removeListener(this) + continuation.resume(Unit) + } + } } - .containsAll(batchedMessages) - - if (allSentMessages.size == count) { - sentEvents.addAll(allSentMessages) + timeline.addListener(timelineListener) + } } - if (hasSyncedAllBatchedMessages) { - timeline.removeListener(this) - latch.countDown() + }, + action = { + batchedMessages.forEach { formattedMessage -> + if (rootThreadEventId != null) { + room.relationService().replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = formattedMessage + ) + } else { + room.sendService().sendTextMessage(formattedMessage) + } } } - } - timeline.addListener(timelineListener) - } + ) } return sentEvents } - fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { - waitWithLatch { latch -> - retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(roomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("# TEST", "${otherSession.myUserId} can see the invite") - } + suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("# TEST", "${otherSession.myUserId} can see the invite") } } } // not sure why it's taking so long :/ - runBlockingTest(90_000) { + wrapWithTimeout(90_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") try { otherSession.roomService().joinRoom(roomID) @@ -275,11 +259,9 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - waitWithLatch { - retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(roomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } @@ -289,7 +271,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: * @param message the message to send * @param numberOfMessages the number of time the message will be sent */ - fun replyInThreadMessage( + suspend fun replyInThreadMessage( room: Room, message: String, numberOfMessages: Int, @@ -307,15 +289,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: // PRIVATE METHODS ***************************************************************************** - /** - * Creates a unique account - * - * @param userNamePrefix the user name prefix - * @param password the password - * @param testParams test params about the session - * @return the session associated with the newly created account - */ - private fun createAccount( + private suspend fun createAccount( userNamePrefix: String, password: String, testParams: SessionTestParams @@ -333,15 +307,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } } - /** - * Logs into an existing account - * - * @param userId the userId to log in - * @param password the password to log in - * @param testParams test params about the session - * @return the session associated with the existing account - */ - fun logIntoAccount( + suspend fun logIntoAccount( userId: String, password: String, testParams: SessionTestParams @@ -353,32 +319,25 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } } - /** - * Create an account and a dedicated session - * - * @param userName the account username - * @param password the password - * @param sessionTestParams parameters for the test - */ - private fun createAccountAndSync( + private suspend fun createAccountAndSync( userName: String, password: String, sessionTestParams: SessionTestParams ): Session { val hs = createHomeServerConfig() - runBlockingTest { + wrapWithTimeout(TestConstants.timeOutMillis) { matrix.authenticationService.getLoginFlow(hs) } - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(60_000L) { matrix.authenticationService .getRegistrationWizard() .createAccount(userName, password, null) } // Perform dummy step - val registrationResult = runBlockingTest(timeout = 60_000) { + val registrationResult = wrapWithTimeout(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .dummy() @@ -393,29 +352,14 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: return session } - /** - * Start an account login - * - * @param userName the account username - * @param password the password - * @param sessionTestParams session test params - */ - private fun logAccountAndSync( - userName: String, - password: String, - sessionTestParams: SessionTestParams - ): Session { + private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): Session { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) - val session = runBlockingTest { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } + val session = matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") session.open() if (sessionTestParams.withInitialSync) { syncSession(session) @@ -430,25 +374,21 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: * @param userName the account username * @param password the password */ - fun logAccountWithError( + suspend fun logAccountWithError( userName: String, password: String ): Throwable { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) var requestFailure: Throwable? = null - runBlockingTest { - try { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } catch (failure: Throwable) { - requestFailure = failure - } + try { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") + } catch (failure: Throwable) { + requestFailure = failure } assertNotNull(requestFailure) @@ -484,65 +424,48 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: ) } - suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { - while (true) { - try { - delay(1000) - } catch (ex: CancellationException) { - // the job was canceled, just stop - return - } - if (condition()) { - latch.countDown() - return + suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) { + wrapWithTimeout(timeout) { + while (!predicate()) { + runBlocking { delay(500) } } } } - fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { - val latch = CountDownLatch(1) - val job = coroutineScope.launch(dispatcher) { - block(latch) - } - await(latch, timeout, job) - } - - fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { - return runBlocking { - withTimeout(timeout) { - block() + suspend fun <T> waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback<T> { + override fun onSuccess(data: T) { + continuation.resume(data) + } + } + block(callback) } } } - // Transform a method with a MatrixCallback to a synchronous method - inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T { - val lock = CountDownLatch(1) - var result: T? = null - - val callback = object : TestMatrixCallback<T>(lock) { - override fun onSuccess(data: T) { - result = data - super.onSuccess(data) + suspend fun <T> waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): Throwable { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback<T> { + override fun onFailure(failure: Throwable) { + continuation.resume(failure) + } + } + block(callback) } } - - block.invoke(callback) - - await(lock, timeout) - - assertNotNull(result) - return result!! } /** * Clear all provided sessions */ - fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } + suspend fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } - fun signOutAndClose(session: Session) { + suspend fun signOutAndClose(session: Session) { trackedSessions.remove(session) - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(timeout = 60_000L) { session.signOutService().signOut(true) } // no need signout will close diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 41d0d3a7e86810386ee6ddfa2bcf43d2517195e0..8cd5bee5698945ccfa401a3a672e6560a50a2e71 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -32,7 +32,7 @@ data class CryptoTestData( val thirdSession: Session? get() = sessions.getOrNull(2) - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { sessions.forEach { testHelper.signOutAndClose(it) } 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 210ce906928ac8c161777018ed0a20da835ccc51..74292daf150adde20e826faf1a11e22730307ce0 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 @@ -16,19 +16,15 @@ package org.matrix.android.sdk.common -import android.os.SystemClock import android.util.Log -import androidx.lifecycle.Observer import org.amshove.kluent.fail import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue 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.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -46,22 +42,16 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction 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 import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.KeyRef -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.toBase64NoPadding import java.util.UUID import kotlin.coroutines.Continuation @@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = roomHistoryVisibility - name = "MyRoom" - }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = roomHistoryVisibility + name = "MyRoom" + }) if (encryptedRoom) { - testHelper.waitWithLatch { latch -> - val room = aliceSession.getRoom(roomId)!! - room.roomCryptoService().enableEncryption() - val roomSummaryLive = room.getRoomSummaryLive() - val roomSummaryObserver = object : Observer<Optional<RoomSummary>> { - override fun onChanged(roomSummary: Optional<RoomSummary>) { - if (roomSummary.getOrNull()?.isEncrypted.orFalse()) { - roomSummaryLive.removeObserver(this) - latch.countDown() - } - } - } - roomSummaryLive.observeForever(roomSummaryObserver) - } + val room = aliceSession.getRoom(roomId)!! + waitFor( + continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } }, + action = { room.roomCryptoService().enableEncryption() } + ) } return CryptoTestData(roomId, listOf(aliceSession)) } @@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(t: List<RoomSummary>?) { - if (t?.isNotEmpty() == true) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - aliceRoom.membershipService().invite(bobSession.myUserId) - } + waitFor( + continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } }, + action = { aliceRoom.membershipService().invite(bobSession.myUserId) } + ) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val roomJoinedObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(t: List<RoomSummary>?) { - if (bobSession.getRoom(aliceRoomId) - ?.membershipService() - ?.getRoomMember(bobSession.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() + waitFor( + continueWhen = { + bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { + bobSession.getRoom(aliceRoomId) + ?.membershipService() + ?.getRoomMember(bobSession.myUserId) + ?.membership == Membership.JOIN } - } - } - bobRoomSummariesLive.observeForever(roomJoinedObserver) - bobSession.roomService().joinRoom(aliceRoomId) - } + }, + action = { bobSession.roomService().joinRoom(aliceRoomId) } + ) + // Ensure bob can send messages to the room // val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! // assertNotNull(roomFromBobPOV.powerLevels) @@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } - /** - * @return Alice, Bob and Sam session - */ - fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceAndBobInARoom() - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - - val room = aliceSession.getRoom(aliceRoomId)!! - - val samSession = createSamAccountAndInviteToTheRoom(room) - - // wait the initial sync - SystemClock.sleep(1000) - - return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) - } - - /** - * Create Sam account and invite him in the room. He will accept the invitation - * @Return Sam session - */ - fun createSamAccountAndInviteToTheRoom(room: Room): Session { - val samSession = testHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) - - testHelper.runBlockingTest { - room.membershipService().invite(samSession.myUserId, null) - } - - testHelper.runBlockingTest { - samSession.roomService().joinRoom(room.roomId, null, emptyList()) - } - - return samSession - } - /** * @return Alice and Bob sessions */ - fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { val cryptoTestData = doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -235,49 +165,20 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return cryptoTestData } - private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) - if (andCanDecrypt) { - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } else { - timeLineEvent != null - } + private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { + testHelper.retryPeriodically { + val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) + if (andCanDecrypt) { + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } else { + timeLineEvent != null } } } - fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { - assertEquals(EventType.ENCRYPTED, event.type) - assertNotNull(event.content) - - val eventWireContent = event.content.toContent() - assertNotNull(eventWireContent) - - assertNull(eventWireContent["body"]) - assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"]) - - assertNotNull(eventWireContent["ciphertext"]) - assertNotNull(eventWireContent["session_id"]) - assertNotNull(eventWireContent["sender_key"]) - - assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"]) - - assertNotNull(event.eventId) - assertEquals(roomId, event.roomId) - assertEquals(EventType.MESSAGE, event.getClearType()) - // TODO assertTrue(event.getAge() < 10000) - - val eventContent = event.toContent() - assertNotNull(eventContent) - assertEquals(clearMessage, eventContent["body"]) - assertEquals(senderSession.myUserId, event.senderId) - } - - fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { return MegolmBackupAuthData( publicKey = "abcdefg", signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) @@ -292,44 +193,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { ) } - fun createDM(alice: Session, bob: Session): String { - var roomId: String = "" - testHelper.waitWithLatch { latch -> - roomId = alice.roomService().createDirectRoom(bob.myUserId) - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(t: List<RoomSummary>?) { - if (t?.any { it.roomId == roomId }.orFalse()) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - } - - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(t: List<RoomSummary>?) { - if (bob.getRoom(roomId) - ?.membershipService() - ?.getRoomMember(bob.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - bob.roomService().joinRoom(roomId) - } + suspend fun createDM(alice: Session, bob: Session): String { + var roomId = "" + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { it.any { it.roomId == roomId }.orFalse() } + }, + action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) } + ) + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { + bob.getRoom(roomId) + ?.membershipService() + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN + } + }, + action = { bob.roomService().joinRoom(roomId) } + ) return roomId } - fun initializeCrossSigning(session: Session) { - testHelper.doSync<Unit> { + suspend fun initializeCrossSigning(session: Session) { + testHelper.waitForCallback<Unit> { session.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -350,57 +242,55 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * Initialize cross-signing, set up megolm backup and save all in 4S */ - fun bootstrapSecurity(session: Session) { + suspend fun bootstrapSecurity(session: Session) { initializeCrossSigning(session) val ssssService = session.sharedSecretStorageService() - testHelper.runBlockingTest { - val keyInfo = ssssService.generateKey( - UUID.randomUUID().toString(), - null, - "ssss_key", - EmptyKeySigner() - ) - ssssService.setDefaultKey(keyInfo.keyId) + val keyInfo = ssssService.generateKey( + UUID.randomUUID().toString(), + null, + "ssss_key", + EmptyKeySigner() + ) + ssssService.setDefaultKey(keyInfo.keyId) - ssssService.storeSecret( - MASTER_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) - ssssService.storeSecret( - SELF_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + // set up megolm backup + val creationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = testHelper.waitForCallback<KeysVersion> { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> ssssService.storeSecret( - USER_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + KEYBACKUP_SECRET_SSSS_NAME, + secret, listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) - - // set up megolm backup - val creationInfo = awaitCallback<MegolmBackupCreationInfo> { - session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) - } - val version = awaitCallback<KeysVersion> { - session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) - - extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> - ssssService.storeSecret( - KEYBACKUP_SECRET_SSSS_NAME, - secret, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) - } } } - fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) @@ -415,30 +305,26 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { roomId = roomId ).transactionId - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { - it.requestInfo?.fromDevice == alice.sessionParams.deviceId - } != null - } + testHelper.retryPeriodically { + bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } != null } val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { it.requestInfo?.fromDevice == alice.sessionParams.deviceId } - bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!) + bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, incomingRequest.transactionId!!) var requestID: String? = null // wait for it to be readied - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) - .firstOrNull { it.localId == localId } - if (outgoingRequest?.isReady == true) { - requestID = outgoingRequest.transactionId!! - true - } else { - false - } + testHelper.retryPeriodically { + val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) + .firstOrNull { it.localId == localId } + if (outgoingRequest?.isReady == true) { + requestID = outgoingRequest.transactionId!! + true + } else { + false } } @@ -454,23 +340,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { var alicePovTx: OutgoingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction - Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + 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}") - if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() - } - bobPovTx?.state == VerificationTxState.ShortCodeReady + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() } + bobPovTx?.state == VerificationTxState.ShortCodeReady } assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) @@ -478,38 +360,30 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { bobPovTx!!.userHasVerifiedShortCode() alicePovTx!!.userHasVerifiedShortCode() - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) } - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId) } } - fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { + suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) val room = aliceSession.getRoom(roomId)!! - testHelper.runBlockingTest { - room.roomCryptoService().enableEncryption() - } + room.roomCryptoService().enableEncryption() val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { val session = testHelper.createAccount("User_$index", defaultSessionParams) - testHelper.runBlockingTest(timeout = 600_000) { room.membershipService().invite(session.myUserId, null) } + room.membershipService().invite(session.myUserId, null) println("TEST -> " + session.myUserId + " invited") - testHelper.runBlockingTest { session.roomService().joinRoom(room.roomId, null, emptyList()) } + session.roomService().joinRoom(room.roomId, null, emptyList()) println("TEST -> " + session.myUserId + " joined") sessions.add(session) } @@ -517,48 +391,43 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(roomId, sessions) } - fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) { + suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) { sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "").let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } - } catch (error: MXCryptoError) { - // nop - } + testHelper.retryPeriodically { + val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root + ?: return@retryPeriodically false + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe + ) } - Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body + } catch (error: MXCryptoError) { + // nop } + Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body } } } - fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { + suspend fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { sentEventIds.forEach { sentEventId -> val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "") - fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - if (expectedError == null) { - assertNotNull(errorType) - } else { - assertEquals("Unexpected reason", expectedError, errorType) - } + try { + session.cryptoService().decryptEvent(event, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + if (expectedError == null) { + assertNotNull(errorType) + } else { + assertEquals("Unexpected reason", expectedError, errorType) } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f89d42ac07211fa9e83ee2ce533a1f3c24879c6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.resume + +suspend fun <T, R> T.onMain(block: T.() -> R): R { + return withContext(Dispatchers.Main) { + block(this@onMain) + } +} + +suspend fun <T> LiveData<T>.first(timeout: Long = TestConstants.timeOutMillis, predicate: (T) -> Boolean): T { + return wrapWithTimeout(timeout) { + withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val observer = object : Observer<T> { + override fun onChanged(data: T) { + if (predicate(data)) { + removeObserver(this) + continuation.resume(data) + } + } + } + observeForever(observer) + continuation.invokeOnCancellation { removeObserver(observer) } + } + } + } +} + +suspend fun <T> waitFor(continueWhen: suspend () -> T, action: suspend () -> Unit) { + coroutineScope { + val deferred = async { continueWhen() } + action() + deferred.await() + } +} + +suspend fun <T> wrapWithTimeout(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { + val deferred = coroutineScope { + async { block() } + } + return withTimeout(timeout) { deferred.await() } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt index a48b45a1f53fb78b819c310679fc274dc90aee20..4e1efbb700bdbe3d2ea700701f88be323b04e940 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -46,30 +46,26 @@ class DecryptRedactedEventTest : InstrumentedTest { roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) // get the event from bob - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true } val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! - testHelper.runBlockingTest { - try { - val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") - Assert.assertEquals( - "Unexpected redacted reason", - redactionReason, - result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason") - ) - Assert.assertEquals( - "Unexpected Redacted event id", - timelineEvent.eventId, - result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts - ) - } catch (failure: Throwable) { - Assert.fail("Should not throw when decrypting a redacted event") - } + try { + val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") + Assert.assertEquals( + "Unexpected redacted reason", + redactionReason, + result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason") + ) + Assert.assertEquals( + "Unexpected Redacted event id", + timelineEvent.eventId, + result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts + ) + } catch (failure: Throwable) { + Assert.fail("Should not throw when decrypting a redacted event") } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt index 32d63a1934241c23e99f0a6bdb7a54436923914b..cbbc4dc74e9874d065c4f9c97fcb69dc0302edfd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -40,7 +40,6 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestData import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -57,18 +56,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -81,9 +76,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) // Let alice invite bob - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(bobSession.myUserId) - } + roomAlice.membershipService().invite(bobSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) @@ -114,9 +107,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(samSession.myUserId) - } + roomAlice.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) @@ -135,7 +126,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(false) @@ -162,7 +153,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) @@ -186,7 +177,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) } - private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> { + private suspend fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> { val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1) val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1) @@ -195,9 +186,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let bob invite sam - commonTestHelper.runBlockingTest { - bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) - } + bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId) return Triple(fromAliceNotSharable, fromBobSharable, samSession) @@ -209,18 +198,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -232,18 +217,15 @@ class E2EShareKeysConfigTest : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val keysBackupService = aliceSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> { + val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> { keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = commonTestHelper.doSync<KeysVersion> { + val version = commonTestHelper.waitForCallback<KeysVersion> { keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } - commonTestHelper.waitWithLatch { latch -> - keysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) + commonTestHelper.waitForCallback<Unit> { + keysBackupService.backupAllGroupSessions(null, it) } // signout @@ -253,11 +235,11 @@ class E2EShareKeysConfigTest : InstrumentedTest { newAliceSession.cryptoService().enableShareKeyOnInvite(true) newAliceSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> { + val keyVersionResult = commonTestHelper.waitForCallback<KeysVersionResult?> { kbs.getVersion(version.version, it) } - val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> { + val importedResult = commonTestHelper.waitForCallback<ImportRoomKeysResult> { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -276,9 +258,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) - } + newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8b12092b794fbb1a070285770a6ee733ecde4a39 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.test.filters.LargeTest +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeConfigTest : InstrumentedTest { + + @Test + fun testBlacklistUnverifiedDefault() = runCryptoTest(context()) { cryptoTestHelper, _ -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestData.firstSession.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false + cryptoTestData.firstSession.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false + cryptoTestData.secondSession!!.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false + cryptoTestData.secondSession!!.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false + } + + @Test + fun testCantDecryptIfGlobalUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null + } + + cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId) + } + + @Test + fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession) + cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!) + + cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId) + + cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null + } + + cryptoTestHelper.ensureCanDecrypt( + listOf(sentMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + listOf(sentMessage.getLastMessageContent()!!.body) + ) + } + + @Test + fun testCantDecryptIfPerRoomUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + + val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! + + val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + + val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! + // ensure other received + testHelper.retryPeriodically { + roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null + } + + cryptoTestHelper.ensureCanDecrypt( + listOf(beforeMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + listOf(beforeMessage.getLastMessageContent()!!.body) + ) + + cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true) + + val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + + // ensure received + testHelper.retryPeriodically { + cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null + } + + cryptoTestHelper.ensureCannotDecrypt( + listOf(afterMessage.eventId), + cryptoTestData.secondSession!!, + cryptoTestData.roomId, + MXCryptoError.ErrorType.KEYS_WITHHELD + ) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index 410fb4f5d4778be9e415eeca3b933eec41605805..a36ba8ac028cedbd26edcbf3e38f4615b4c242b3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -18,17 +18,25 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.filters.LargeTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 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.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -56,12 +64,12 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest -import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.mustFail -import java.util.concurrent.CountDownLatch +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") @RunWith(JUnit4::class) @@ -69,8 +77,6 @@ import java.util.concurrent.CountDownLatch @LargeTest class E2eeSanityTests : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) - /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -103,10 +109,8 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "All accounts created") // we want to invite them in the room otherAccounts.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } // All user should accept invite @@ -128,14 +132,12 @@ class E2eeSanityTests : InstrumentedTest { // All should be able to decrypt otherAccounts.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE && - timeLineEvent.root.mxDecryptionResult?.isSafe == true - } + testHelper.retryPeriodically { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE && + timeLineEvent.root.mxDecryptionResult?.isSafe == true } } @@ -146,10 +148,8 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } newAccount.forEach { @@ -159,21 +159,17 @@ class E2eeSanityTests : InstrumentedTest { ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) // wait a bit - testHelper.runBlockingTest { - delay(3_000) - } + delay(3_000) // check that messages are encrypted (uisi) newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { - Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED && - timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { + Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.ENCRYPTED && + timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID } } @@ -185,15 +181,13 @@ class E2eeSanityTests : InstrumentedTest { // new members should be able to decrypt it newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { - Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.MESSAGE && - secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { + Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.MESSAGE && + secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body } } } @@ -229,10 +223,10 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val bobKeysBackupService = bobSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { + val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> { bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = testHelper.doSync<KeysVersion> { + val version = testHelper.waitForCallback<KeysVersion> { bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } Log.v("#E2E TEST", "... Key backup started and enabled for bob") @@ -248,32 +242,21 @@ class E2eeSanityTests : InstrumentedTest { sentEventIds.add(it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } // we want more so let's discard the session aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - - testHelper.runBlockingTest { - delay(1_000) - } } Log.v("#E2E TEST", "Bob received all and can decrypt") // Let's wait a bit to be sure that bob has backed up the session Log.v("#E2E TEST", "Force key backup for Bob...") - testHelper.waitWithLatch { latch -> - bobKeysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) - } + testHelper.waitForCallback<Unit> { bobKeysBackupService.backupAllGroupSessions(null, it) } Log.v("#E2E TEST", "... Key backup done for Bob") // Now lets logout both alice and bob to ensure that we won't have any gossiping @@ -284,9 +267,7 @@ class E2eeSanityTests : InstrumentedTest { testHelper.signOutAndClose(bobSession) Log.v("#E2E TEST", "..Logout alice and bob...") - testHelper.runBlockingTest { - delay(1_000) - } + delay(1_000) // Create a new session for bob Log.v("#E2E TEST", "Create a new session for Bob") @@ -295,14 +276,11 @@ class E2eeSanityTests : InstrumentedTest { // check that bob can't currently decrypt Log.v("#E2E TEST", "check that bob can't currently decrypt") sentEventIds.forEach { sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { - Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED + testHelper.retryPeriodically { + val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { + Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") } + timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED } } // after initial sync events are not decrypted, so we have to try manually @@ -311,11 +289,11 @@ class E2eeSanityTests : InstrumentedTest { // Let's now import keys from backup newBobSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = testHelper.doSync<KeysVersionResult?> { + val keyVersionResult = testHelper.waitForCallback<KeysVersionResult?> { kbs.getVersion(version.version, it) } - val importedResult = testHelper.doSync<ImportRoomKeysResult> { + val importedResult = testHelper.waitForCallback<ImportRoomKeysResult> { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -335,9 +313,7 @@ class E2eeSanityTests : InstrumentedTest { // Check key trust sentEventIds.forEach { sentEventId -> val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!! - val result = testHelper.runBlockingTest { - newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") - } + val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") assertEquals("Keys from history should be deniable", false, result.isSafe) } } @@ -366,13 +342,11 @@ class E2eeSanityTests : InstrumentedTest { sentEventIds.add(it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -396,27 +370,24 @@ class E2eeSanityTests : InstrumentedTest { } // Ensure that new bob still can't decrypt (keys must have been withheld) - // as per new config we won't request to alice, so ignore following test // sentEventIds.forEach { sentEventId -> // val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! // .getTimelineEvent(sentEventId)!! // .root.content.toModel<EncryptedEventContent>()!!.sessionId -// testHelper.waitWithLatch { latch -> -// testHelper.retryPeriodicallyWithLatch(latch) { -// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() -// .first { -// it.sessionId == megolmSessionId && -// it.roomId == e2eRoomID -// } -// .results.also { -// Log.w("##TEST", "result list is $it") -// } -// .firstOrNull { it.userId == aliceSession.myUserId } -// ?.result -// aliceReply != null && -// aliceReply is RequestResult.Failure && -// WithHeldCode.UNAUTHORISED == aliceReply.code -// } +// testHelper.retryPeriodically { +// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() +// .first { +// it.sessionId == megolmSessionId && +// it.roomId == e2eRoomID +// } +// .results.also { +// Log.w("##TEST", "result list is $it") +// } +// .firstOrNull { it.userId == aliceSession.myUserId } +// ?.result +// aliceReply != null && +// aliceReply is RequestResult.Failure && +// WithHeldCode.UNAUTHORISED == aliceReply.code // } // } @@ -460,13 +431,11 @@ class E2eeSanityTests : InstrumentedTest { firstMessage.let { text -> firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -488,13 +457,11 @@ class E2eeSanityTests : InstrumentedTest { secondMessage.let { text -> secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -508,18 +475,14 @@ class E2eeSanityTests : InstrumentedTest { Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) // Confirm we can decrypt one but not the other - testHelper.runBlockingTest { - mustFail(message = "Should not be able to decrypt event") { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } + mustFail(message = "Should not be able to decrypt event") { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") } - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt event") - } + try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt event") } // Now let's verify bobs session, and re-request keys @@ -538,50 +501,42 @@ class E2eeSanityTests : InstrumentedTest { // old session should have shared the key at earliest known index now // we should be able to decrypt both - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val canDecryptFirst = try { - testHelper.runBlockingTest { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } - true - } catch (error: MXCryptoError) { - false - } - val canDecryptSecond = try { - testHelper.runBlockingTest { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } - true - } catch (error: MXCryptoError) { - false - } - canDecryptFirst && canDecryptSecond + testHelper.retryPeriodically { + val canDecryptFirst = try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false } + val canDecryptSecond = try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false + } + canDecryptFirst && canDecryptSecond } } - private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendService().sendTextMessage(text) + private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { var sentEventId: String? = null - testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> - val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) - timeline.start() - testHelper.retryPeriodicallyWithLatch(latch) { - val decryptedMsg = timeline.getSnapshot() - .filter { it.root.getClearType() == EventType.MESSAGE } - .also { list -> - val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } - Log.v("#E2E TEST", "Timeline snapshot is $message") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } + aliceRoomPOV.sendService().sendTextMessage(text) - timeline.dispose() + val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + testHelper.retryPeriodically { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } + Log.v("#E2E TEST", "Timeline snapshot is $message") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null } + timeline.dispose() return sentEventId } @@ -598,106 +553,35 @@ class E2eeSanityTests : InstrumentedTest { val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) - val oldCompleteLatch = CountDownLatch(1) - lateinit var oldCode: String - aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { - - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - val readyInfo = pr.readyInfo - if (readyInfo != null) { - aliceSession.cryptoService().verificationService().beginKeyVerification( - VerificationMethod.SAS, - aliceSession.myUserId, - readyInfo.fromDevice, - readyInfo.transactionId - - ) - } - } - - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "exitsingPov: $tx") - val sasTx = tx as OutgoingSasVerificationTransaction - when (sasTx.uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - // for the test we just accept? - oldCode = sasTx.getDecimalCodeRepresentation() - sasTx.userHasVerifiedShortCode() - } - OutgoingSasVerificationTransaction.UxState.VERIFIED -> { - // we can release this latch? - oldCompleteLatch.countDown() - } - else -> Unit - } - } - }) - - val newCompleteLatch = CountDownLatch(1) - lateinit var newCode: String - aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { - - override fun verificationRequestCreated(pr: PendingVerificationRequest) { - // let's ready - aliceNewSession.cryptoService().verificationService().readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceSession.myUserId, - pr.transactionId!! - ) - } - - var matchOnce = true - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "newPov: $tx") - - val sasTx = tx as IncomingSasVerificationTransaction - when (sasTx.uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - // no need to accept as there was a request first it will auto accept - } - IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - if (matchOnce) { - sasTx.userHasVerifiedShortCode() - newCode = sasTx.getDecimalCodeRepresentation() - matchOnce = false - } - } - IncomingSasVerificationTransaction.UxState.VERIFIED -> { - newCompleteLatch.countDown() - } - else -> Unit - } - } - }) - + val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId) + val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId) // initiate self verification aliceSession.cryptoService().verificationService().requestKeyVerification( listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), aliceNewSession.myUserId, listOf(aliceNewSession.sessionParams.deviceId!!) ) - testHelper.await(oldCompleteLatch) - testHelper.await(newCompleteLatch) + + val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode) + assertEquals("Decimal code should have matched", oldCode, newCode) // Assert that devices are verified - val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) - val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) + val newDeviceFromOldPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) // wait for secret gossiping to happen - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() - } + testHelper.retryPeriodically { + aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null - } + testHelper.retryPeriodically { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null } assertEquals( @@ -730,27 +614,191 @@ class E2eeSanityTests : InstrumentedTest { ) } - private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN + @Test + fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + testHelper.waitForCallback { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume(aliceAuthParams) + } + }, it) + } + + testHelper.waitForCallback { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume(bobAuthParams) } + }, it) + } + + // add a second session for bob but not cross signed + + val secondBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + // The two bob session should not be able to decrypt any message + + val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!! + Timber.v("#TEST: Send a first message that should be withheld") + val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!! + + // wait for it to be synced back the other side + Timber.v("#TEST: Wait for message to be synced back") + testHelper.retryPeriodically { + bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null + } + + testHelper.retryPeriodically { + secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null + } + + // bob should not be able to decrypt + Timber.v("#TEST: Ensure cannot be decrytped") + cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), bobSession, cryptoTestData.roomId) + cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), secondBobSession, cryptoTestData.roomId) + + // let's try to verify, it should work even if bob devices are untrusted + Timber.v("#TEST: Do the verification") + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) + + Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt") + + val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!! + Timber.v("#TEST: Wait for message to be synced back") + testHelper.retryPeriodically { + bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null + } + + testHelper.retryPeriodically { + secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null + } + + cryptoTestHelper.ensureCanDecrypt(listOf(secondEvent), bobSession, cryptoTestData.roomId, listOf("World")) + cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId) + } + + private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> { + return scope.async { + suspendCancellableCoroutine { continuation -> + var oldCode: String? = null + val listener = object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + val readyInfo = pr.readyInfo + if (readyInfo != null) { + beginKeyVerification( + VerificationMethod.SAS, + userId, + readyInfo.fromDevice, + readyInfo.transactionId + + ) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "exitsingPov: $tx") + val sasTx = tx as OutgoingSasVerificationTransaction + when (sasTx.uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + // for the test we just accept? + oldCode = sasTx.getDecimalCodeRepresentation() + sasTx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + removeListener(this) + // we can release this latch? + continuation.resume(oldCode!!) + } + else -> Unit + } + } + } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } } - private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - sentEventIds.forEach { sentEventId -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE + private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> { + return scope.async { + suspendCancellableCoroutine { continuation -> + var newCode: String? = null + + val listener = object : VerificationService.Listener { + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // let's ready + readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + userId, + pr.transactionId!! + ) + } + + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "newPov: $tx") + + val sasTx = tx as IncomingSasVerificationTransaction + when (sasTx.uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + // no need to accept as there was a request first it will auto accept + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + sasTx.userHasVerifiedShortCode() + newCode = sasTx.getDecimalCodeRepresentation() + matchOnce = false + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + removeListener(this) + continuation.resume(newCode!!) + } + else -> Unit + } + } } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } + } + } + } + + private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + + private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) { + sentEventIds.forEach { sentEventId -> + testHelper.retryPeriodically { + val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 4b44aab18b8b0030a48e279d7b3539282b80eaa5..91e0026c93d72b2ccaae21b12fad19a46f966b20 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.wrapWithTimeout @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -102,8 +102,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") // Bob should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && @@ -113,7 +112,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } } - } } // Create a new user @@ -123,10 +121,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Aris user created") // Alice invites new user to the room - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") - aliceRoomPOV.membershipService().invite(arisSession.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") + aliceRoomPOV.membershipService().invite(arisSession.myUserId) waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) @@ -139,8 +135,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { null -> { // Aris should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null && timelineEvent.isEncrypted() && @@ -151,19 +146,16 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } } - } } } RoomHistoryVisibility.INVITED, RoomHistoryVisibility.JOINED -> { // Aris should not even be able to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) - ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) - timelineEvent == null - } + testHelper.retryPeriodically { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + timelineEvent == null } } } @@ -241,10 +233,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { private fun testRotationDueToVisibilityChange( initRoomHistoryVisibility: RoomHistoryVisibility, nextRoomHistoryVisibility: RoomHistoryVisibilityContent - ) { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - + ) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId @@ -270,96 +259,84 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { // Bob should be able to decrypt the message var firstAliceMessageMegolmSessionId: String? = null val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) + } + } + } + + Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) + + var secondAliceMessageSessionId: String? = null + sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> + testHelper.retryPeriodically { val timelineEvent = bobRoomPov ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) + ?.getTimelineEvent(secondMessage) (timelineEvent != null && timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) { - firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String Log.v( "#E2E TEST", - "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" ) } } } } - - Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) - - var secondAliceMessageSessionId: String? = null - sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(secondMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - Log.v( - "#E2E TEST", - "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" - ) - } - } - } - } - } assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId) Log.v("#E2E TEST ROTATION", "No rotation needed yet") // Let's change the room history visibility - testHelper.runBlockingTest { - aliceRoomPOV.stateService() - .sendStateEvent( - eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, - stateKey = "", - body = RoomHistoryVisibilityContent( - historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr - ).toContent() - ) - } + aliceRoomPOV.stateService() + .sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + body = RoomHistoryVisibilityContent( + historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr + ).toContent() + ) // ensure that the state did synced down - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content - ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content + ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomVisibility = aliceSession.getRoom(e2eRoomID)!! - .stateService() - .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) - ?.content - ?.toModel<RoomHistoryVisibilityContent>() - Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") - roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + val roomVisibility = aliceSession.getRoom(e2eRoomID)!! + .stateService() + .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.toModel<RoomHistoryVisibilityContent>() + Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") + roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } var aliceThirdMessageSessionId: String? = null sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(thirdMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - } + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(thirdMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String } } } @@ -379,38 +356,34 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { + private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let { Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}") return it.eventId } } - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } + private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN } } } - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } + private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") } } } - testHelper.runBlockingTest(60_000) { + wrapWithTimeout(60_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") try { otherSession.roomService().joinRoom(e2eRoomID) @@ -420,11 +393,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } } 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 e8e7b1d708444b9589884837a559a8e0ca442e9f..5c817443ce6adc0d9d828c2d964ce0d877f2d692 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 @@ -52,15 +52,13 @@ class PreShareKeysTest : InstrumentedTest { Log.d("#Test", "Room Key Received from alice $preShareCount") // Force presharing of new outbound key - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - newKeysCount > preShareCount - } + testHelper.retryPeriodically { + val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() + newKeysCount > preShareCount } val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting @@ -85,10 +83,8 @@ class PreShareKeysTest : InstrumentedTest { val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE } } } 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 130c8d13f9909a242ee33e12118f8b0ee437125f..889cc9a562ae6bf2003ba67a478b43855817a734 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 @@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBe +import kotlinx.coroutines.suspendCancellableCoroutine +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Before import org.junit.FixMethodOrder @@ -46,7 +47,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.olm.OlmSession import timber.log.Timber -import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -102,69 +102,37 @@ class UnwedgingTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) bobTimeline.start() - val bobFinalLatch = CountDownLatch(1) - val bobHasThreeDecryptedEventsListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List<String>) { - // noop - } - - override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { - val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } - Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages") - if (decryptedEventReceivedByBob.size == 3) { - if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - bobFinalLatch.countDown() - } - } - } - } - bobTimeline.addListener(bobHasThreeDecryptedEventsListener) - - var latch = CountDownLatch(1) - var bobEventsListener = createEventListener(latch, 1) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() // - Alice sends a 1st message with a 1st megolm session roomFromAlicePOV.sendService().sendTextMessage("First message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1) - messagesReceivedByBob.size shouldBe 1 + messagesReceivedByBob.size shouldBeEqualTo 1 val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! // - 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()!!) - sessionIdsForBob!!.size shouldBe 1 + sessionIdsForBob!!.size shouldBeEqualTo 1 val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! val oldSession = serializeForRealm(olmSession.olmSession) aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - Thread.sleep(6_000) - latch = CountDownLatch(1) - bobEventsListener = createEventListener(latch, 2) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() - Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") // - Alice sends a 2nd message with a 2nd megolm session roomFromAlicePOV.sendService().sendTextMessage("Second message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2) - messagesReceivedByBob.size shouldBe 2 + messagesReceivedByBob.size shouldBeEqualTo 2 // Session should have changed val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! Assert.assertNotEquals(firstMessageSession, secondMessageSession) @@ -177,25 +145,18 @@ class UnwedgingTest : InstrumentedTest { bobSession.cryptoService().getMyDevice().identityKey()!! ) olmDevice.clearOlmSessionCache() - Thread.sleep(6_000) // Force new session, and key share aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") + // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + roomFromAlicePOV.sendService().sendTextMessage("Third message") + // Bob should not be able to decrypt, because the session key could not be sent // Wait for the message to be received by Bob - testHelper.waitWithLatch { - bobEventsListener = createEventListener(it, 3) - bobTimeline.addListener(bobEventsListener) - messagesReceivedByBob = emptyList() - - Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") - // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - roomFromAlicePOV.sendService().sendTextMessage("Third message") - // Bob should not be able to decrypt, because the session key could not be sent - } - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3) - messagesReceivedByBob.size shouldBe 3 + messagesReceivedByBob.size shouldBeEqualTo 3 val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") @@ -205,11 +166,11 @@ class UnwedgingTest : InstrumentedTest { Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged - testHelper.await(bobFinalLatch) - bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) + + Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // It's a trick to force key request on fail to decrypt - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -227,24 +188,22 @@ class UnwedgingTest : InstrumentedTest { } // Wait until we received back the key - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - // we should get back the key and be able to decrypt - val result = testHelper.runBlockingTest { - tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") - } - } - Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") - result != null + testHelper.retryPeriodically { + // we should get back the key and be able to decrypt + val result = tryOrNull { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") } + Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") + result != null } bobTimeline.dispose() } +} - private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { - return object : Timeline.Listener { +private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> { + return suspendCancellableCoroutine { continuation -> + val listener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { // noop } @@ -254,12 +213,16 @@ class UnwedgingTest : InstrumentedTest { } override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { - messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED } - if (messagesReceivedByBob.size == expectedNumberOfMessages) { - latch.countDown() + if (messagesReceived.size == expectedCount) { + removeListener(this) + continuation.resume(messagesReceived) } } } + + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } 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 ef3fdfeeda301d6f214a1d3e8ebdead50363e457..c4fb89693421e4c36edd99e17860dd33217bdd25 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 @@ -25,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -42,20 +41,20 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -@Ignore class XSigningTest : InstrumentedTest { @Test fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { @@ -101,14 +100,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { promise.resume(bobAuthParams) @@ -117,7 +116,7 @@ class XSigningTest : InstrumentedTest { } // Check that alice can see bob keys - testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) @@ -154,14 +153,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { promise.resume(bobAuthParams) @@ -171,12 +170,12 @@ class XSigningTest : InstrumentedTest { // Check that alice can see bob keys val bobUserId = bobSession.myUserId - testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - testHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } // 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 @@ -185,7 +184,7 @@ class XSigningTest : InstrumentedTest { val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login - val data = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + val data = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } @@ -197,12 +196,12 @@ class XSigningTest : InstrumentedTest { assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) } // Now alice should cross trust bob's second device - val data2 = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { + val data2 = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } @@ -214,4 +213,104 @@ class XSigningTest : InstrumentedTest { val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) } + + @Test + fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + testHelper.waitForCallback<Unit> { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume(aliceAuthParams) + } + }, it) + } + testHelper.waitForCallback<Unit> { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume(bobAuthParams) + } + }, it) + } + + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) + + testHelper.retryPeriodically { + aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + testHelper.retryPeriodically { + aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId).isVerified() + } + + aliceSession.cryptoService() + // Ensure also that bob device is trusted + testHelper.retryPeriodically { + val deviceInfo = aliceSession.cryptoService().getUserDevices(bobSession.myUserId).firstOrNull() + Timber.v("#TEST device:${deviceInfo?.shortDebugString()} trust ${deviceInfo?.trustLevel}") + deviceInfo?.trustLevel?.crossSigningVerified == true + } + + val currentBobMSK = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId)!! + .masterKey()!!.unpaddedBase64PublicKey!! + + testHelper.waitForCallback<Unit> { + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { + promise.resume(bobAuthParams) + } + }, it) + } + + testHelper.retryPeriodically { + val newBobMsk = aliceSession.cryptoService().crossSigningService() + .getUserCrossSigningKeys(bobSession.myUserId) + ?.masterKey()?.unpaddedBase64PublicKey + newBobMsk != null && newBobMsk != currentBobMSK + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + // assert that bob is not trusted anymore from alice s + testHelper.retryPeriodically { + val trust = aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId) + !trust.isVerified() + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.retryPeriodically { + val info = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + info?.wasTrustedOnce == true + } + + // trick to force event to sync + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping() + bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping() + + testHelper.retryPeriodically { + !aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId) + } + + // Ensure also that bob device are not trusted + testHelper.retryPeriodically { + aliceSession.cryptoService().getUserDevices(bobSession.myUserId).first().trustLevel?.crossSigningVerified != true + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index 5f26fda94618019f119284305fa27a2360fbf50a..42a04dbe3f9003a3330416c469a796e0df0f1152 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test @@ -34,54 +34,59 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestHelper -import java.util.concurrent.CountDownLatch +import org.matrix.android.sdk.common.waitFor +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class EncryptionTest : InstrumentedTest { @Test - fun test_EncryptionEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room -> - // Send an encryption Event as an Event (and not as a state event) - room.sendService().sendEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } + fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room -> + // Send an encryption Event as an Event (and not as a state event) + room.sendService().sendEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } @Test - fun test_EncryptionStateEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room -> - runBlocking { - // Send an encryption Event as a State Event - room.stateService().sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = "", - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } - } + fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room -> + // Send an encryption Event as a State Event + room.stateService().sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } - private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) { + private suspend fun performTest(cryptoTestHelper: CryptoTestHelper, roomShouldBeEncrypted: Boolean, action: suspend (Room) -> Unit) { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false) - val aliceSession = cryptoTestData.firstSession val room = aliceSession.getRoom(cryptoTestData.roomId)!! room.roomCryptoService().isEncrypted() shouldBe false val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) - val latch = CountDownLatch(1) + timeline.start() + waitFor( + continueWhen = { timeline.waitForEncryptedMessages() }, + action = { action.invoke(room) } + ) + timeline.dispose() + + room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted + } +} + +private suspend fun Timeline.waitForEncryptedMessages() { + suspendCancellableCoroutine<Unit> { continuation -> val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest { .filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION } if (newMessages.isNotEmpty()) { - timeline.removeListener(this) - latch.countDown() + removeListener(this) + continuation.resume(Unit) } } } - timeline.start() - timeline.addListener(timelineListener) - - action.invoke(room) - testHelper.await(latch) - timeline.dispose() - testHelper.waitWithLatch { - room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted - it.countDown() - } + addListener(timelineListener) + continuation.invokeOnCancellation { removeListener(timelineListener) } } } 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 df0b10ea6d9143e49b856c5d268fe19413ab6715..8e001b84d38836dc57e3c790959b8133f4c6c93a 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 @@ -62,14 +62,12 @@ class KeyShareTests : InstrumentedTest { Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom( - CreateRoomParams().apply { - visibility = RoomDirectoryVisibility.PRIVATE - enableEncryption() - } - ) - } + val roomId = aliceSession.roomService().createRoom( + CreateRoomParams().apply { + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() + } + ) val room = aliceSession.getRoom(roomId) assertNotNull(room) Thread.sleep(4_000) @@ -93,10 +91,8 @@ class KeyShareTests : InstrumentedTest { assertNotNull(receivedEvent) assert(receivedEvent!!.isEncrypted()) - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -110,15 +106,13 @@ class KeyShareTests : InstrumentedTest { var outGoingRequestId: String? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession2.cryptoService().getOutgoingRoomKeyRequests() - .let { - val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } - outGoingRequestId = outgoing?.requestId - outgoing != null - } - } + commonTestHelper.retryPeriodically { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } } Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId") @@ -130,47 +124,41 @@ class KeyShareTests : InstrumentedTest { // The first session should see an incoming request // the request should be refused, because the device is not trusted - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceSession.cryptoService().getIncomingRoomKeyRequests().let { - Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") - Log.v("#TEST", "=========================") - it.forEach { keyRequest -> - Log.v( - "#TEST", - "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}" - ) - } - Log.v("#TEST", "=========================") + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceSession.cryptoService().getIncomingRoomKeyRequests().let { + Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") + Log.v("#TEST", "=========================") + it.forEach { keyRequest -> + Log.v( + "#TEST", + "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}" + ) } - - val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - incoming != null + Log.v("#TEST", "=========================") } - } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> - Log.v("#TEST", "=========================") - Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") - Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") - Log.v("#TEST", "=========================") - } + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null + } - val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val resultCode = (reply?.result as? RequestResult.Failure)?.code - resultCode == WithHeldCode.UNVERIFIED + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("#TEST", "=========================") + Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("#TEST", "=========================") } + + val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val resultCode = (reply?.result as? RequestResult.Failure)?.code + resultCode == WithHeldCode.UNVERIFIED } - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } // Mark the device as trusted @@ -228,12 +216,10 @@ class KeyShareTests : InstrumentedTest { // As it was share previously alice should accept to reshare bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - aliceReply != null && aliceReply.result is RequestResult.Success - } + commonTestHelper.retryPeriodically { + val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + aliceReply != null && aliceReply.result is RequestResult.Success } } @@ -254,12 +240,10 @@ class KeyShareTests : InstrumentedTest { val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) // we wait for alice first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!! @@ -267,13 +251,11 @@ class KeyShareTests : InstrumentedTest { // As it was share previously alice should accept to reshare aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success } } @@ -300,12 +282,10 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.syncSession(aliceNewSession) // we wait bob first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -327,26 +307,22 @@ class KeyShareTests : InstrumentedTest { aliceNewSession.cryptoService().enableKeyGossiping(true) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = outgoing?.results - ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - Log.v("TEST", "own device result is $result") - result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + Log.v("TEST", "own device result is $result") + result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobDeviceReply = outgoing?.results - ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } - val result = bobDeviceReply?.result - Log.v("TEST", "bob device result is $result") - result != null && result is RequestResult.Success && result.chainIndex > 0 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } + val result = bobDeviceReply?.result + Log.v("TEST", "bob device result is $result") + result != null && result is RequestResult.Success && result.chainIndex > 0 } // it's a success but still can't decrypt first message @@ -363,21 +339,19 @@ class KeyShareTests : InstrumentedTest { // Let's now try to request aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> - Log.v("TEST", "=========================") - Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") - Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") - Log.v("TEST", "=========================") - } - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 0 + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("TEST", "=========================") + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("TEST", "=========================") } + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 } // now the new session should be able to decrypt all! @@ -389,13 +363,11 @@ class KeyShareTests : InstrumentedTest { ) // Additional test, can we check that bob replied successfully but with a ratcheted key - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } - val result = bobReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 3 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 } commonTestHelper.signOutAndClose(aliceNewSession) @@ -423,12 +395,10 @@ class KeyShareTests : InstrumentedTest { val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) // we wait bob first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -462,14 +432,12 @@ class KeyShareTests : InstrumentedTest { aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) // Should get a reply from bob and not from alice - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } - val result = bobReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 3 - } + commonTestHelper.retryPeriodically { + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 } val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } @@ -482,14 +450,12 @@ class KeyShareTests : InstrumentedTest { aliceSession.syncService().startSync(true) // We should now get a reply from first session - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 0 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 } // It should be in sent then cancel 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 910a349b403ac108b7f7809dfde68600a6bb690c..b55ddbc97065684813bbc158ca870071e9f4c25f 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 @@ -81,10 +81,8 @@ class WithHeldTests : InstrumentedTest { val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() // await for bob unverified session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null - } + testHelper.retryPeriodically { + bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null } val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! @@ -96,60 +94,52 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) - } - ) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + ) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } // Let's see if the reply we got from bob first session is unverified - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() - .firstOrNull { it.sessionId == megolmSessionId } - ?.results - ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } - ?.result - ?.let { - it as? RequestResult.Failure - } - ?.code == WithHeldCode.UNVERIFIED - } + testHelper.retryPeriodically { + bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() + .firstOrNull { it.sessionId == megolmSessionId } + ?.results + ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } + ?.result + ?.let { + it as? RequestResult.Failure + } + ?.code == WithHeldCode.UNVERIFIED } // enable back sending to unverified aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) - // wait until it's decrypted - ev?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE } // Previous message should still be undecryptable (partially withheld session) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) - }) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + }) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } } @@ -181,26 +171,22 @@ class WithHeldTests : InstrumentedTest { val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId // await for bob session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null - } + testHelper.retryPeriodically { + bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null } // Previous message should still be undecryptable (partially withheld session) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) - }) { - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + }) { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") } // Ensure that alice has marked the session to be shared with bob @@ -220,10 +206,8 @@ class WithHeldTests : InstrumentedTest { // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null - } + testHelper.retryPeriodically { + bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null } val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( @@ -265,27 +249,21 @@ class WithHeldTests : InstrumentedTest { var sessionId: String? = null // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { - // try to decrypt and force key request - tryOrNull { - testHelper.runBlockingTest { - bobSecondSession.cryptoService().decryptEvent(it.root, "") - } - } + testHelper.retryPeriodically { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { + // try to decrypt and force key request + tryOrNull { + bobSecondSession.cryptoService().decryptEvent(it.root, "") } - sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId - timeLineEvent != null } + sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId + timeLineEvent != null } // Check that bob second session requested the key - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) - wc?.code == WithHeldCode.UNAUTHORISED - } + testHelper.retryPeriodically { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt index cf201611a0c0aa02d591a99d72fab3c13d273a92..8679cf3c998f7ba4a98f1d1eedf8aca55f9f09dd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -30,7 +30,7 @@ internal data class KeysBackupScenarioData( val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { cryptoTestData.cleanUp(testHelper) testHelper.signOutAndClose(aliceSession2) } 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 2439119f01c8eb1ddcd0abd8cc91d103cf2903fe..01c03b8001615cc1d0fbac7d7a7bf88b773b2d2e 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 @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import kotlinx.coroutines.suspendCancellableCoroutine 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.FixMethodOrder import org.junit.Rule @@ -48,9 +48,11 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.common.waitFor +import java.security.InvalidParameterException import java.util.Collections import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -116,7 +118,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { + val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> { keysBackup.prepareKeysBackupVersion(null, null, it) } @@ -133,7 +135,6 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) cryptoTestHelper.initializeCrossSigning(bobSession) @@ -143,14 +144,14 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { + val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> { keysBackup.prepareKeysBackupVersion(null, null, it) } assertFalse(keysBackup.isEnabled()) // Create the version - val version = testHelper.doSync<KeysVersion> { + val version = testHelper.waitForCallback<KeysVersion> { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -158,10 +159,10 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Check that it's signed with MSK - val versionResult = testHelper.doSync<KeysVersionResult?> { + val versionResult = testHelper.waitForCallback<KeysVersionResult?> { keysBackup.getVersion(version.version, it) } - val trust = testHelper.doSync<KeysBackupVersionTrust> { + val trust = testHelper.waitForCallback<KeysBackupVersionTrust> { keysBackup.getKeysBackupTrust(versionResult!!, it) } @@ -257,7 +258,7 @@ class KeysBackupTest : InstrumentedTest { var lastBackedUpKeysProgress = 0 - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { assertEquals(nbOfKeys, total) @@ -299,7 +300,7 @@ class KeysBackupTest : InstrumentedTest { val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo // - Check encryptGroupSession() returns stg - val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } + val keyBackupData = keysBackup.encryptGroupSession(session) assertNotNull(keyBackupData) assertNotNull(keyBackupData!!.sessionData) @@ -334,7 +335,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) // - Restore the e2e backup from the homeserver - val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { + val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -379,7 +380,7 @@ class KeysBackupTest : InstrumentedTest { // assertTrue(unsentRequest != null || sentRequest != null) // // // - Restore the e2e backup from the homeserver -// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { +// val importRoomKeysResult = mTestHelper.doSyncSuspending<> { }<ImportRoomKeysResult> { // testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, // testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, // null, @@ -429,7 +430,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, true, @@ -445,14 +446,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { + val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { + val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -489,7 +490,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the recovery key - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -505,14 +506,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { + val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { + val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -547,13 +548,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong recovery key - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "Bad recovery key", - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError<Unit> { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "Bad recovery key", + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -591,7 +592,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the password - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -607,14 +608,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { + val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { + val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -652,13 +653,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong password - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - badPassword, - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError<Unit> { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + badPassword, + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -679,26 +680,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong recovery key - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null, - null, - null, - object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> { + keysBackupService.restoreKeysWithRecoveryKey( + keysBackupService.keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -718,7 +714,7 @@ class KeysBackupTest : InstrumentedTest { // - Restore the e2e backup with the password val steps = ArrayList<StepProgressListener.Step>() - val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { + val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> { testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -771,26 +767,21 @@ class KeysBackupTest : InstrumentedTest { val wrongPassword = "passw0rd" val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - wrongPassword, - null, - null, - null, - object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + wrongPassword, + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -808,7 +799,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) // - Restore the e2e backup with the recovery key. - val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { + val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -833,26 +824,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "password", - null, - null, - null, - object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + "password", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is IllegalStateException) } /** @@ -874,12 +860,12 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Get key backup version from the homeserver - val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { + val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> { keysBackup.getCurrentVersion(it) }.toKeysVersionResult() // - Check the returned KeyBackupVersion is trusted - val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { + val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> { keysBackup.getKeysBackupTrust(keysVersionResult!!, it) } @@ -918,34 +904,39 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) // Wait for keys backup to be finished - val latch0 = CountDownLatch(1) var count = 0 - keysBackup.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (newState == KeysBackupState.ReadyToBackUp) { - count++ - - if (count == 2) { - // Remove itself from the list of listeners - keysBackup.removeListener(this) - - latch0.countDown() + waitFor( + continueWhen = { + suspendCancellableCoroutine<Unit> { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + continuation.resume(Unit) + } + } + } + } + keysBackup.addListener(listener) + continuation.invokeOnCancellation { keysBackup.removeListener(listener) } } - } - } - }) - - // - Make alice back up her keys to her homeserver - keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + action = { + // - Make alice back up her keys to her homeserver + keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + ) assertTrue(keysBackup.isEnabled()) - testHelper.await(latch0) - // - Create a new backup with fake data on the homeserver, directly using the rest client val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo() - testHelper.doSync<KeysVersion> { + testHelper.waitForCallback<KeysVersion> { (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -953,9 +944,7 @@ class KeysBackupTest : InstrumentedTest { (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() // - Make alice back up all her keys again - val latch2 = CountDownLatch(1) - keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) - testHelper.await(latch2) + testHelper.waitForCallbackError<Unit> { keysBackup.backupAllGroupSessions(null, it) } // -> That must fail and her backup state must be WrongBackUpVersion assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) @@ -991,7 +980,7 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Wait for keys backup to finish by asking again to backup keys. - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { keysBackup.backupAllGroupSessions(null, it) } @@ -1016,19 +1005,7 @@ class KeysBackupTest : InstrumentedTest { val stateObserver2 = StateObserver(keysBackup2) - var isSuccessful = false - val latch2 = CountDownLatch(1) - keysBackup2.backupAllGroupSessions( - null, - object : TestMatrixCallback<Unit>(latch2, false) { - override fun onSuccess(data: Unit) { - isSuccessful = true - super.onSuccess(data) - } - }) - testHelper.await(latch2) - - assertFalse(isSuccessful) + testHelper.waitForCallbackError<Unit> { keysBackup2.backupAllGroupSessions(null, it) } // Backup state must be NotTrusted assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) @@ -1042,24 +1019,25 @@ class KeysBackupTest : InstrumentedTest { ) // -> Backup should automatically enable on the new device - val latch4 = CountDownLatch(1) - keysBackup2.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { - // Remove itself from the list of listeners - keysBackup2.removeListener(this) - - latch4.countDown() + suspendCancellableCoroutine<Unit> { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + continuation.resume(Unit) + } } } - }) - testHelper.await(latch4) + keysBackup2.addListener(listener) + continuation.invokeOnCancellation { keysBackup2.removeListener(listener) } + } // -> It must use the same backup version assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) } @@ -1092,7 +1070,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Delete the backup - testHelper.doSync<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + testHelper.waitForCallback<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } // Backup is now disabled assertFalse(keysBackup.isEnabled()) 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 2cc2b506b925322552f247fbb6f002c0e889fbbd..10abf93bcb071948fb4e900f234a49d52470b3ea 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 @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session @@ -29,7 +30,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.assertDictEquals import org.matrix.android.sdk.common.assertListEquals import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume internal class KeysBackupTestHelper( private val testHelper: CommonTestHelper, @@ -47,7 +48,7 @@ internal class KeysBackupTestHelper( * * @param password optional password */ - fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + suspend fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() waitForKeybackUpBatching() @@ -64,7 +65,7 @@ internal class KeysBackupTestHelper( var lastProgress = 0 var lastTotal = 0 - testHelper.doSync<Unit> { + testHelper.waitForCallback<Unit> { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { lastProgress = progress @@ -97,13 +98,13 @@ internal class KeysBackupTestHelper( ) } - fun prepareAndCreateKeysBackupData( + suspend fun prepareAndCreateKeysBackupData( keysBackup: KeysBackupService, password: String? = null ): PrepareKeysBackupDataResult { val stateObserver = StateObserver(keysBackup) - val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { + val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> { keysBackup.prepareKeysBackupVersion(password, null, it) } @@ -112,7 +113,7 @@ internal class KeysBackupTestHelper( Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) // Create the version - val keysVersion = testHelper.doSync<KeysVersion> { + val keysVersion = testHelper.waitForCallback<KeysVersion> { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -129,25 +130,26 @@ internal class KeysBackupTestHelper( * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the * KeysBackup object to be in the specified state */ - fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + suspend fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { // If already in the wanted state, return - if (session.cryptoService().keysBackupService().getState() == state) { + val keysBackupService = session.cryptoService().keysBackupService() + if (keysBackupService.getState() == state) { return } // Else observe state changes - val latch = CountDownLatch(1) - - session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - if (newState == state) { - session.cryptoService().keysBackupService().removeListener(this) - latch.countDown() + suspendCancellableCoroutine<Unit> { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + keysBackupService.removeListener(this) + continuation.resume(Unit) + } } } - }) - - testHelper.await(latch) + keysBackupService.addListener(listener) + continuation.invokeOnCancellation { keysBackupService.removeListener(listener) } + } } fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt index 53cf802b91e9978a09906cff4fbc7e757db675fd..0dfecffbded67ed21b0698b145df5aa5c5a7e63c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -58,18 +58,16 @@ class ReplayAttackTest : InstrumentedTest { val fakeEventWithTheSameIndex = sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId)) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - // Lets decrypt the fake event that will have the same message index - val exception = assertFailsWith<MXCryptoError.Base> { - // An exception should be thrown while the same index would have been used for the previous decryption - aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) - } - assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + // Lets decrypt the fake event that will have the same message index + val exception = assertFailsWith<MXCryptoError.Base> { + // An exception should be thrown while the same index would have been used for the previous decryption + aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) } + assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) cryptoTestData.cleanUp(testHelper) } @@ -93,17 +91,15 @@ class ReplayAttackTest : InstrumentedTest { Assert.assertTrue("Message should be sent", sentEvents.size == 1) assertEquals(sentEvents.size, 1) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + try { + // Lets try to decrypt the same event aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - try { - // Lets try to decrypt the same event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - } catch (ex: Throwable) { - fail("Shouldn't throw a decryption error for same event") - } + } catch (ex: Throwable) { + fail("Shouldn't throw a decryption error for same event") } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index c8be6aae74ec8a6bcb24a9a5ce4e36e238c591d7..0467d082a33f2952237a8b1309f93acfe2e0485d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.ssss -import androidx.lifecycle.Observer import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -37,12 +36,12 @@ import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo -import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService @RunWith(AndroidJUnit4::class) @@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - var accountData: UserAccountDataEvent? = null // Assert Account data is updated - testHelper.waitWithLatch { - val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") - val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> - if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { - accountData = t.getOrNull() - } - it.countDown() - } - liveAccountData.observeForever(accountDataObserver) - } + val accountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") } + .first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" } + .getOrNull() + assertNotNull("Key should be stored in account data", accountData) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) assertNotNull("Key Content cannot be parsed", parsed) @@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest { assertEquals("Unexpected key name", "Test Key", parsed.name) assertNull("Key was not generated from passphrase", parsed.passphrase) - var defaultKeyAccountData: UserAccountDataEvent? = null + quadS.setDefaultKey(TEST_KEY_ID) + val defaultKeyAccountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } + .first { it.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID } + .getOrNull() + // Set as default key - testHelper.waitWithLatch { latch -> - quadS.setDefaultKey(TEST_KEY_ID) - val liveDefAccountData = - aliceSession.accountDataService().getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) - val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> - if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { - defaultKeyAccountData = t.getOrNull()!! - latch.countDown() - } - } - liveDefAccountData.observeForever(accountDefDataObserver) - } assertNotNull(defaultKeyAccountData?.content) assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) @@ -112,21 +96,19 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId = "My.Key" - val info = generatedSecret(testHelper, aliceSession, keyId, true) + val info = generatedSecret(aliceSession, keyId, true) val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) // Store a secret val clearSecret = "42".toByteArray().toBase64NoPadding() - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "secret.of.life", - clearSecret, - listOf(KeyRef(null, keySpec)) // default key - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "secret.of.life", + clearSecret, + listOf(KeyRef(null, keySpec)) // default key + ) - val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life") + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> assertNotNull("Element should be encrypted", encryptedContent) @@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest { // Try to decrypt?? - val decryptedSecret = testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "secret.of.life", - null, // default key - keySpec!! - ) - } + val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret( + "secret.of.life", + null, // default key + keySpec!! + ) assertEquals("Secret mismatch", clearSecret, decryptedSecret) } @@ -159,14 +139,10 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) // Test that we don't need to wait for an account data sync to access directly the keyid from DB - testHelper.runBlockingTest { - quadS.setDefaultKey(TEST_KEY_ID) - } + quadS.setDefaultKey(TEST_KEY_ID) } @Test @@ -174,22 +150,20 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" - val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true) + val key1Info = generatedSecret(aliceSession, keyId1, true) val keyId2 = "Key2" - val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true) + val key2Info = generatedSecret(aliceSession, keyId2, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf( - KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), - KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) - ) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ) + ) val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret") val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> @@ -200,21 +174,17 @@ class QuadSTests : InstrumentedTest { assertNotNull(encryptedContent?.get(keyId2)) // Assert that can decrypt with both keys - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! - ) - } - - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId2, - RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! - ) - } + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! + ) + + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! + ) } @Test @@ -224,104 +194,84 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val passphrase = "The good pass phrase" - val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true) + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) + ) - testHelper.runBlockingTest { - try { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromPassphrase( - "A bad passphrase", - key1Info.content?.passphrase?.salt ?: "", - key1Info.content?.passphrase?.iterations ?: 0, - null - ) - ) - } catch (throwable: Throwable) { - assert(throwable is SharedSecretStorageError.BadMac) - } - } - - // Now try with correct key - testHelper.runBlockingTest { + try { aliceSession.sharedSecretStorageService().getSecret( "my.secret", keyId1, RawBytesKeySpec.fromPassphrase( - passphrase, + "A bad passphrase", key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, null ) ) + } catch (throwable: Throwable) { + assert(throwable is SharedSecretStorageError.BadMac) } + + // Now try with correct key + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null + ) + ) } - private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent { - var accountData: UserAccountDataEvent? = null - testHelper.waitWithLatch { - val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) - val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> - if (t?.getOrNull()?.type == type) { - accountData = t.getOrNull() - it.countDown() - } - } - liveAccountData.observeForever(accountDataObserver) - } + private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountData = session.accountDataService() + .onMain { getLiveUserAccountDataEvent(type) } + .first { it.getOrNull()?.type == type } + .getOrNull() + assertNotNull("Account Data type:$type should be found", accountData) return accountData!! } - private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKey(keyId, null, keyId, emptyKeySigner) - } + val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo } - private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKeyWithPassphrase( - keyId, - keyId, - passphrase, - emptyKeySigner, - null - ) - } + val creationInfo = quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null + ) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo 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 1bffbeeeaa9dd9d22eba064e57cef491a35c275e..fd2136edd5f0dabd612e3c3dc81a27f599afddd7 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 @@ -547,23 +547,19 @@ class SASTest : InstrumentedTest { var requestID: String? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - requestID = prAlicePOV?.transactionId - Log.v("TEST", "== alicePOV is $prAlicePOV") - prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId } Log.v("TEST", "== requestID is $requestID") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() - Log.v("TEST", "== prBobPOV is $prBobPOV") - prBobPOV?.transactionId == requestID - } + testHelper.retryPeriodically { + val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID } bobVerificationService.readyPendingVerification( @@ -573,12 +569,10 @@ class SASTest : InstrumentedTest { ) // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - Log.v("TEST", "== prAlicePOV is $prAlicePOV") - prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null } // Start concurrent! @@ -602,20 +596,16 @@ class SASTest : InstrumentedTest { var alicePovTx: SasVerificationTransaction? var bobPovTx: SasVerificationTransaction? - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== alicePovTx is $alicePovTx") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady } // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== bobPovTx is $bobPovTx") - bobPovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady } } } 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/qrcode/VerificationTest.kt index 3f229069653067c43b4e0b13c1d5c95d72519994..4ecfe5be8f31ca3f5741e5aa029401f82ee2c5e2 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/qrcode/VerificationTest.kt @@ -164,7 +164,7 @@ class VerificationTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! - testHelper.doSync<Unit> { callback -> + testHelper.waitForCallback<Unit> { callback -> aliceSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -181,7 +181,7 @@ class VerificationTest : InstrumentedTest { ) } - testHelper.doSync<Unit> { callback -> + testHelper.waitForCallback<Unit> { callback -> bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -261,7 +261,11 @@ class VerificationTest : InstrumentedTest { val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) - val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) + val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount( + aliceSessionToVerify.myUserId, + TestConstants.PASSWORD, + defaultSessionParams + ) val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) @@ -286,11 +290,9 @@ class VerificationTest : InstrumentedTest { otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId), ) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) - requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } - } + testHelper.retryPeriodically { + val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) + requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } } testHelper.signOutAndClose(aliceSessionToVerify) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index 59b3b1453229c135340bba60a27f831b0ca78c22..656e00bcbdf89404e5a91f545150f3b3599550b7 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.session.room.timeline import androidx.test.filters.LargeTest -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.internal.assertEquals import org.junit.FixMethodOrder import org.junit.Ignore @@ -35,6 +35,9 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.waitFor +import org.matrix.android.sdk.common.wrapWithTimeout +import kotlin.coroutines.resume @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -69,30 +72,36 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30)) bobTimeline.start() - commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { - val listener = object : Timeline.Listener { + waitFor( + continueWhen = { + wrapWithTimeout(timeout = TestConstants.timeOutMillis * 10) { + suspendCancellableCoroutine<Unit> { continuation -> + val listener = object : Timeline.Listener { - override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if (direction == Timeline.Direction.FORWARDS) { - return + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if (direction == Timeline.Direction.FORWARDS) { + return + } + if (state.hasMoreToLoad && !state.loading) { + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } else if (!state.hasMoreToLoad) { + bobTimeline.removeListener(this) + continuation.resume(Unit) + } + } + } + bobTimeline.addListener(listener) + continuation.invokeOnCancellation { bobTimeline.removeListener(listener) } + } } - if (state.hasMoreToLoad && !state.loading) { - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } else if (!state.hasMoreToLoad) { - bobTimeline.removeListener(this) - it.countDown() - } - } - } - bobTimeline.addListener(listener) - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } + }, + action = { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) } + ) + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) - val onlySentEvents = runBlocking { - bobTimeline.getSnapshot() - } + val onlySentEvents = bobTimeline.getSnapshot() .filter { it.root.isTextMessage() }.filter { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 7c97426c391a675f4c2659b030e12ef214162eb6..6ef90193d8010f76a15f86c6bd996fdf3dc8859a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -85,9 +85,7 @@ class SearchMessagesTest : InstrumentedTest { 2 ) - val data = commonTestHelper.runBlockingTest { - block.invoke(cryptoTestData) - } + val data = block.invoke(cryptoTestData) assertTrue(data.results?.size == 2) assertTrue( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 2cd579df24dfec8056199cf1c48cc539656b6d3d..df131cc19aa542c43516a5e351ab4fa09782a6b0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -55,15 +55,11 @@ class SpaceCreationTest : InstrumentedTest { val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" val topic = "A public space for test" - var spaceId: String = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(roomName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(roomName, topic, null, true) - commonTestHelper.waitWithLatch { - commonTestHelper.retryPeriodicallyWithLatch(it) { - session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null - } + commonTestHelper.retryPeriodically { + val roomSummary = session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary() + roomSummary?.name == roomName && roomSummary.topic == topic } val syncedSpace = session.spaceService().getSpace(spaceId) @@ -79,14 +75,12 @@ class SpaceCreationTest : InstrumentedTest { assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) var powerLevelsContent: PowerLevelsContent? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - powerLevelsContent = syncedSpace.asRoom() - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel<PowerLevelsContent>() - powerLevelsContent != null - } + commonTestHelper.retryPeriodically { + powerLevelsContent = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel<PowerLevelsContent>() + powerLevelsContent != null } assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) @@ -116,19 +110,13 @@ class SpaceCreationTest : InstrumentedTest { val roomName = "My Space" val topic = "A public space for test" - val spaceId: String - runBlocking { - spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) - // wait a bit to let the summary update it self :/ - delay(400) - } + val spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summary update it self :/ + delay(400) // Try to join from bob, it's a public space no need to invite - val joinResult: JoinSpaceResult - runBlocking { - joinResult = bobSession.spaceService().joinSpace(spaceId) - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) @@ -152,43 +140,24 @@ class SpaceCreationTest : InstrumentedTest { val syncedSpace = aliceSession.spaceService().getSpace(spaceId) // create a room - var firstChild: String? = null - commonTestHelper.waitWithLatch { - firstChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "FirstRoom" - this.topic = "Description of first room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val firstChild: String = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "FirstRoom" + this.topic = "Description of first room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - var secondChild: String? = null - commonTestHelper.waitWithLatch { - secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "SecondRoom" - this.topic = "Description of second room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "SecondRoom" + this.topic = "Description of second room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) // Try to join from bob, it's a public space no need to invite - var joinResult: JoinSpaceResult? = null - commonTestHelper.waitWithLatch { - joinResult = bobSession.spaceService().joinSpace(spaceId) - // wait a bit to let the summary update it self :/ - it.countDown() - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 18645fd6d9ce82cf5323a03a56db9d17140a56f2..abe9af5e3859b9c915f75c96c18dac9d72ff7a2f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.session.space import android.util.Log -import androidx.lifecycle.Observer -import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -39,16 +37,17 @@ import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain +import org.matrix.android.sdk.common.waitFor @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -60,40 +59,28 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" val topic = "A public space for test" - var spaceId = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(spaceName, topic, null, true) val syncedSpace = session.spaceService().getSpace(spaceId) - var roomId = "" - commonTestHelper.runBlockingTest { - roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) - } + val roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - syncedSpace!!.addChildren(roomId, viaServers, null, true) - } + syncedSpace!!.addChildren(roomId, viaServers, null, true) - commonTestHelper.runBlockingTest { - session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - } + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents - val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } - parents?.forEach { - Log.d("## TEST", "parent : $it") - } - parents?.size == 1 && - parents.first().roomSummary?.name == spaceName && - canonicalParents?.size == 1 && - canonicalParents.first().roomSummary?.name == spaceName + commonTestHelper.retryPeriodically { + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + parents?.forEach { + Log.d("## TEST", "parent : $it") } + parents?.size == 1 && + parents.first().roomSummary?.name == spaceName && + canonicalParents?.size == 1 && + canonicalParents.first().roomSummary?.name == spaceName } } @@ -169,7 +156,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -178,7 +164,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) /* val spaceBInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -188,7 +173,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -199,22 +183,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // Create orphan rooms - - var orphan1 = "" - commonTestHelper.runBlockingTest { - orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) - } - - var orphan2 = "" - commonTestHelper.runBlockingTest { - orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) - } + val orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) + val orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) @@ -235,15 +209,15 @@ class SpaceHierarchyTest : InstrumentedTest { assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) // Add a non canonical child and check that it does not appear as orphan - commonTestHelper.runBlockingTest { - val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) - spaceA!!.addChildren(a3, viaServers, null, false) - } + val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) + spaceA.addChildren(a3, viaServers, null, false) + + val orphansUpdate = session.roomService().onMain { + getRoomSummariesLive(roomSummaryQueryParams { + spaceFilter = SpaceFilter.OrphanRooms + }) + }.first { it.size == 2 } - Thread.sleep(6_000) - val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams { - spaceFilter = SpaceFilter.OrphanRooms - }) assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size) } @@ -253,7 +227,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -262,7 +235,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -273,16 +245,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // add back A as subspace of C - commonTestHelper.runBlockingTest { - val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) - spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) - } + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) // A -> C -> A @@ -300,7 +268,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( @@ -310,7 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( @@ -323,13 +289,10 @@ class SpaceHierarchyTest : InstrumentedTest { // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( @@ -338,52 +301,39 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.waitWithLatch { latch -> - - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(children: List<RoomSummary>?) { -// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + waitFor( + continueWhen = { + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + children.any { it.name == "C1" } && children.any { it.name == "C2" } } + }, + action = { + // add C as subspace of B + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) } - } - - flatAChildren.observeForever(childObserver) - - // add C as subspace of B - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - - // C1 and C2 should be in flatten child of A now - } + ) + // C1 and C2 should be in flatten child of A now // Test part one of the rooms val bRoomId = spaceBInfo.roomIds.first() - commonTestHelper.waitWithLatch { latch -> - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer<List<RoomSummary>> { - override fun onChanged(children: List<RoomSummary>?) { - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.roomId == bRoomId } == false) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + waitFor( + continueWhen = { + // The room should have disappear from flat children + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + !children.any { it.roomId == bRoomId } } + }, + action = { + // part from b room + session.roomService().leaveRoom(bRoomId) } - } + ) - // The room should have disapear from flat children - flatAChildren.observeForever(childObserver) - // part from b room - session.roomService().leaveRoom(bRoomId) - } commonTestHelper.signOutAndClose(session) } @@ -392,68 +342,57 @@ class SpaceHierarchyTest : InstrumentedTest { val roomIds: List<String> ) - private fun createPublicSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPublicSpace( session: Session, spaceName: String, childInfo: List<Triple<String, Boolean, Boolean?>> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List<String> = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - - roomIds = childInfo.map { entry -> - session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = childInfo.map { entry -> + session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) } - private fun createPrivateSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPrivateSpace( session: Session, spaceName: String, childInfo: List<Triple<String, Boolean, Boolean?>> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List<String> = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - roomIds = - childInfo.map { entry -> - val homeServerCapabilities = session - .homeServerCapabilitiesService() - .getHomeServerCapabilities() - session.roomService().createRoom(CreateRoomParams().apply { - name = entry.first - this.featurePreset = RestrictedRoomPreset( - homeServerCapabilities, - listOf( - RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) - ) - ) - }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + val roomIds = childInfo.map { entry -> + val homeServerCapabilities = session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + session.roomService().createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) @@ -464,7 +403,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -473,7 +411,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -483,7 +420,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -494,10 +430,8 @@ class SpaceHierarchyTest : InstrumentedTest { val viaServers = listOf(session.sessionParams.homeServerHost ?: "") // add C as subspace of B - runBlocking { - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - } + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) // Thread.sleep(4_000) // + A @@ -507,11 +441,9 @@ class SpaceHierarchyTest : InstrumentedTest { // + C // + c1, c2 - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } - rootSpaces.size == 2 - } + commonTestHelper.retryPeriodically { + val rootSpaces = session.spaceService().getRootSpaceSummaries() + rootSpaces.size == 2 } } @@ -521,7 +453,6 @@ class SpaceHierarchyTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val spaceAInfo = createPrivateSpace( - commonTestHelper, aliceSession, "Private Space A", listOf( Triple("General", true /*suggested*/, true/*canonical*/), @@ -529,85 +460,58 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.runBlockingTest { - aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) - } + aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) - commonTestHelper.runBlockingTest { - bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) - } + bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) - var bobRoomId = "" - commonTestHelper.runBlockingTest { - bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) - bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - } + val bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) + bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - commonTestHelper.runBlockingTest { - aliceSession.roomService().joinRoom(bobRoomId) - } + aliceSession.roomService().joinRoom(bobRoomId) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true - } + commonTestHelper.retryPeriodically { + aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true } - commonTestHelper.runBlockingTest { - bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - } + bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) - stateEvent != null - } + commonTestHelper.retryPeriodically { + val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) + stateEvent != null } // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space - commonTestHelper.runBlockingTest { - // we can see the state event - // but it is not valid and room is not in hierarchy - assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) - } + // we can see the state event + // but it is not valid and room is not in hierarchy + assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) // Let's now try to make alice admin of the room - commonTestHelper.waitWithLatch { - val room = bobSession.getRoom(bobRoomId)!! - val currentPLContent = room - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - .toModel<PowerLevelsContent>() + val room = bobSession.getRoom(bobRoomId)!! + val currentPLContent = room + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + .toModel<PowerLevelsContent>() - val newPowerLevelsContent = currentPLContent - ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) - ?.toContent() + val newPowerLevelsContent = currentPLContent + ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) + ?.toContent() - room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) - it.countDown() - } + room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel<PowerLevelsContent>() - ?.let { PowerLevelsHelper(it) } - powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) - } + commonTestHelper.retryPeriodically { + val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel<PowerLevelsContent>() + ?.let { PowerLevelsHelper(it) } + powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) } - commonTestHelper.waitWithLatch { - aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - it.countDown() - } + aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true - } + commonTestHelper.retryPeriodically { + bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true } } @@ -616,7 +520,6 @@ class SpaceHierarchyTest : InstrumentedTest { val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -625,7 +528,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -641,51 +543,39 @@ class SpaceHierarchyTest : InstrumentedTest { val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId) val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId) - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(B1roomId, viaServers, null, true) - } + spaceA!!.addChildren(B1roomId, viaServers, null, true) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) } val newAName = "FooBar" - commonTestHelper.runBlockingTest { - spaceA!!.asRoom().stateService().updateName(newAName) - } + spaceA.asRoom().stateService().updateName(newAName) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(newAName) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(newAName) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(newAName) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(newAName) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 893e90fb3efbafe59c44b5125522b082e63aadbf..711956361797bf47068b030c31c272ee08c55f10 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -70,4 +70,8 @@ data class MatrixConfiguration( * List of network interceptors, they will be added when building an OkHttp client. */ val networkInterceptors: List<Interceptor> = emptyList(), + /** + * Sync configuration. + */ + val syncConfig: SyncConfig = SyncConfig(), ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9753e2407f7dd334b5dda410e72a3068bc4189c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api + +data class SyncConfig( + /** + * Time to keep sync connection alive for before making another request in milliseconds. + */ + val longPollTimeout: Long = 30_000L, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt index e701e0f3ba0c4505c73a9633a6bbf78515b84e7e..234a8eee9861f5577720c55fd60c3a39241ac4f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt @@ -131,11 +131,10 @@ class SecretStoringUtils @Inject constructor( * * The secret is encrypted using the following method: AES/GCM/NoPadding */ - @SuppressLint("NewApi") @Throws(Exception::class) fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray { return when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias) + buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> encryptBytesM(secret, keyAlias) else -> encryptBytes(secret, keyAlias) } } @@ -156,10 +155,9 @@ class SecretStoringUtils @Inject constructor( } } - @SuppressLint("NewApi") fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> saveSecureObjectM(keyAlias, output, any) else -> saveSecureObject(keyAlias, output, any) } } @@ -189,7 +187,6 @@ class SecretStoringUtils @Inject constructor( return cipher } - @SuppressLint("NewApi") @RequiresApi(Build.VERSION_CODES.M) private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) 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 e0e662c7893f994c058c40bc20202b3254dd4a86..d2aa8020e8257abbd48807a884a9450afa7c02c5 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 @@ -61,6 +61,8 @@ interface CryptoService { fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> + fun setWarnOnUnknownDevices(warn: Boolean) fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) @@ -77,6 +79,8 @@ interface CryptoService { fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> + /** * Enable or disable key gossiping. * Default is true. @@ -100,7 +104,7 @@ interface CryptoService { */ fun isShareKeysOnInviteEnabled(): Boolean - fun setRoomUnBlacklistUnverifiedDevices(roomId: String) + fun setRoomUnBlockUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -112,7 +116,7 @@ interface CryptoService { suspend fun exportRoomKeys(password: String): ByteArray - fun setRoomBlacklistUnverifiedDevices(roomId: String) + fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..6405652a6846acc0037743a42fec2258d18362aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt @@ -0,0 +1,23 @@ +/* + * 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.api.session.crypto + +data class GlobalCryptoConfig( + val globalBlockUnverifiedDevices: Boolean, + val globalEnableKeyGossiping: Boolean, + val enableKeyForwardingOnInvite: Boolean, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt index 9604decd62928492065c808c8cb6caf2a6c41431..30a2cfd719261636942e9157cacfd119c272e551 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning data class MXCrossSigningInfo( val userId: String, - val crossSigningKeys: List<CryptoCrossSigningKey> + val crossSigningKeys: List<CryptoCrossSigningKey>, + val wasTrustedOnce: Boolean ) { fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true && diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt index b144069b9947b49f5eeeb10959eab881c53f763d..500d0160028fc9bedf2cc0834dbed461305f12f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt @@ -52,9 +52,17 @@ data class DeviceInfo( * The last ip address. */ @Json(name = "last_seen_ip") - val lastSeenIp: String? = null + val lastSeenIp: String? = null, + + @Json(name = "org.matrix.msc3852.last_seen_user_agent") + val unstableLastSeenUserAgent: String? = null, + + @Json(name = "last_seen_user_agent") + val lastSeenUserAgent: String? = null, ) : DatedObject { override val date: Long get() = lastSeenTs ?: 0 + + fun getBestLastSeenUserAgent() = lastSeenUserAgent ?: unstableLastSeenUserAgent } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt new file mode 100644 index 0000000000000000000000000000000000000000..e3c7057b6b6aaa109b0d1636a76fa8d3e3645f8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.model + +enum class UserVerificationLevel { + + VERIFIED_ALL_DEVICES_TRUSTED, + + VERIFIED_WITH_DEVICES_UNTRUSTED, + + UNVERIFIED_BUT_WAS_PREVIOUSLY, + + WAS_NEVER_VERIFIED, +} 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 84c25776e74ca5bea65cc617b434e809d1dc0a69..3ad4f3a87f08c7c03113541477989c7ec65670da 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 @@ -128,4 +128,17 @@ object EventType { type == CALL_REJECT || type == CALL_REPLACES } + + fun isVerificationEvent(type: String): Boolean { + return when (type) { + KEY_VERIFICATION_START, + KEY_VERIFICATION_ACCEPT, + KEY_VERIFICATION_KEY, + KEY_VERIFICATION_MAC, + KEY_VERIFICATION_CANCEL, + KEY_VERIFICATION_DONE, + KEY_VERIFICATION_READY -> true + else -> false + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt index 900a2e237f175da98dfd44f7ccae9745c54e0a10..c8c328c92ca570f73e8a1314ab82c54e62e8d789 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.util +import androidx.annotation.ChecksSdkIntAtLeast + interface BuildVersionSdkIntProvider { /** * Return the current version of the Android SDK. @@ -26,9 +28,13 @@ interface BuildVersionSdkIntProvider { * Checks the if the current OS version is equal or greater than [version]. * @return A `non-null` result if true, `null` otherwise. */ + @ChecksSdkIntAtLeast(parameter = 0, lambda = 1) fun <T> whenAtLeast(version: Int, result: () -> T): T? { return if (get() >= version) { result() } else null } + + @ChecksSdkIntAtLeast(parameter = 0) + fun isAtLeast(version: Int) = get() >= version } 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 322f297ac30ae61625f2df17ededcf13e89ac431..9c3e0ba1c588be1261e842632180ace421220cb2 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 @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest @@ -855,10 +856,12 @@ internal class DefaultCryptoService @Inject constructor( * Handle a key event. * * @param event the key event. + * @param acceptUnrequested, if true it will force to accept unrequested keys. */ private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) { val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return - Timber.tag(loggerTag.value).i("onRoomKeyEvent(forceAccept:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") return @@ -1161,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getGlobalBlacklistUnverifiedDevices() } + override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> { + return cryptoStore.getLiveGlobalCryptoConfig() + } + /** * Tells whether the client should encrypt messages only for the verified devices * in this room. @@ -1169,39 +1176,28 @@ internal class DefaultCryptoService @Inject constructor( * @param roomId the room id * @return true if the client should encrypt messages only for the verified devices. */ -// TODO add this info in CryptoRoomEntity? override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { - return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) } + return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) } ?: false } /** - * Manages the room black-listing for unverified devices. + * A live status regarding sharing keys for unverified devices in this room. * - * @param roomId the room id - * @param add true to add the room id to the list, false to remove it. + * @return Live status */ - private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) { - val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() - - if (add) { - if (roomId !in roomIds) { - roomIds.add(roomId) - } - } else { - roomIds.remove(roomId) - } - - cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> { + return cryptoStore.getLiveBlockUnverifiedDevices(roomId) } /** * Add this room to the ones which don't encrypt messages to unverified devices. * * @param roomId the room id + * @param block if true will block sending keys to unverified devices */ - override fun setRoomBlacklistUnverifiedDevices(roomId: String) { - setRoomBlacklistUnverifiedDevices(roomId, true) + override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { + cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) } /** @@ -1209,8 +1205,8 @@ internal class DefaultCryptoService @Inject constructor( * * @param roomId the room id */ - override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) { - setRoomBlacklistUnverifiedDevices(roomId, false) + override fun setRoomUnBlockUnverifiedDevices(roomId: String) { + setRoomBlockUnverifiedDevices(roomId, false) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 48b46523048c2d24a00c6c8f67f8767108c4534b..faadf339e970c148689658136fe0e562b3c6c6b8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -603,6 +603,7 @@ internal class MXOlmDevice @Inject constructor( * @param keysClaimed Other keys the sender claims. * @param exportFormat true if the megolm keys are in export format * @param sharedHistory MSC3061, this key is sharable on invite + * @param trusted True if the key is coming from a trusted source * @return true if the operation succeeds. */ fun addInboundGroupSession( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index e2ddd5d19f9598e6d9b31c1f1ec32c9144e41473..d9fd5f10ce60994985bb5bdf54e8c16a93e663cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -41,6 +41,7 @@ internal interface IMXDecrypting { * * @param event the key event. * @param defaultKeysBackupService the keys backup service + * @param forceAccept the keys backup service */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 5354cbff3bc52753981d3ed25d4fe22ba2987bc2..64bd52dd3b0dbabafc4f4e29b8c3b2d790336ff2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -187,6 +187,7 @@ internal class MXMegolmDecryption( * * @param event the key event. * @param defaultKeysBackupService the keys backup service + * @param forceAccept if true will force to accept the forwarded key */ override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") @@ -274,7 +275,8 @@ internal class MXMegolmDecryption( if (!shouldAcceptForward) { Timber.tag(loggerTag.value) - .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}, fromInitiator:$isFromSessionInitiator") + .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," + + " fromInitiator:$isFromSessionInitiator") return } } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index fca6fab66c27bbdc0d402dabf5561b4e62d3514c..0b7af9f4d760029baf0d243538c61e62178b412c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -31,6 +31,8 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder import org.matrix.android.sdk.internal.crypto.MXOlmDevice @@ -92,7 +94,18 @@ internal class MXMegolmEncryption( ): Content { val ts = clock.epochMillis() Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") - val devices = getDevicesInRoom(userIds) + + /** + * When using in-room messages and the room has encryption enabled, + * clients should ensure that encryption does not hinder the verification. + * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s + * unverified devices receive the keys necessary to decrypt the messages, + * even if they would normally not be given the keys to decrypt messages in the room. + */ + val shouldSendToUnverified = isVerificationEvent(eventType, eventContent) + + val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified) + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") val outboundSession = ensureOutboundSession(devices.allowedDevices) @@ -107,6 +120,11 @@ internal class MXMegolmEncryption( } } + private fun isVerificationEvent(eventType: String, eventContent: Content) = + EventType.isVerificationEvent(eventType) || + (eventType == EventType.MESSAGE && + eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST) + private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) { // offload to computation thread cryptoCoroutineScope.launch(coroutineDispatchers.computation) { @@ -416,15 +434,17 @@ internal class MXMegolmEncryption( * This method must be called in getDecryptingThreadHandler() thread. * * @param userIds the user ids whose devices must be checked. + * @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if + * such devices are blocked in crypto settings */ - private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo { + private suspend fun getDevicesInRoom(userIds: List<String>, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via // an m.new_device. val keys = deviceListManager.downloadKeys(userIds, false) val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || - cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + cryptoStore.getBlockUnverifiedDevices(roomId) val devicesInRoom = DeviceInRoomInfo() val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>() @@ -444,7 +464,7 @@ internal class MXMegolmEncryption( continue } - if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) { devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) continue } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt index 42629b617e6dd8e1aeb59f239e1e7a03d374c4a4..9235cd2abf5e46edf192beb59f8d8c12d5bb08d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt @@ -31,7 +31,7 @@ import java.util.concurrent.Executors import javax.inject.Inject import kotlin.math.abs -private val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 +private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 @SessionScope internal class UnRequestedForwardManager @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index d405bdce273ce9ecc4753f6132736a5fca2c9ef2..f4796155c62d5cb397fc1bb1136569baaf8701a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -60,7 +60,7 @@ import javax.inject.Inject @SessionScope internal class DefaultCrossSigningService @Inject constructor( - @UserId private val userId: String, + @UserId private val myUserId: String, @SessionId private val sessionId: String, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, @@ -127,7 +127,7 @@ internal class DefaultCrossSigningService @Inject constructor( } // Recover local trust in case private key are there? - setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified()) } } catch (e: Throwable) { // Mmm this kind of a big issue @@ -167,9 +167,13 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onSuccess(data: InitializeCrossSigningTask.Result) { - val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo)) + val crossSigningInfo = MXCrossSigningInfo( + myUserId, + listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo), + true + ) cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } @@ -266,7 +270,7 @@ internal class DefaultCrossSigningService @Inject constructor( uskKeyPrivateKey: String?, sskPrivateKey: String? ): UserTrustResult { - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId) + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) var masterKeyIsTrusted = false var userKeyIsTrusted = false @@ -330,7 +334,7 @@ internal class DefaultCrossSigningService @Inject constructor( val checkSelfTrust = checkSelfTrust() if (checkSelfTrust.isVerified()) { cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) - setUserKeysAsTrusted(userId, true) + setUserKeysAsTrusted(myUserId, true) } return checkSelfTrust } @@ -351,7 +355,7 @@ internal class DefaultCrossSigningService @Inject constructor( * . */ override fun isUserTrusted(otherUserId: String): Boolean { - return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true } override fun isCrossSigningVerified(): Boolean { @@ -363,7 +367,7 @@ internal class DefaultCrossSigningService @Inject constructor( */ override fun checkUserTrust(otherUserId: String): UserTrustResult { Timber.v("## CrossSigning checkUserTrust for $otherUserId") - if (otherUserId == userId) { + if (otherUserId == myUserId) { return checkSelfTrust() } // I trust a user if I trust his master key @@ -371,16 +375,14 @@ internal class DefaultCrossSigningService @Inject constructor( // TODO what if the master key is signed by a device key that i have verified // First let's get my user key - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) - - checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - return UserTrustResult.Success + return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) } fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { val myUserKey = myCrossSigningInfo?.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) if (!myCrossSigningInfo.isTrusted()) { return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) @@ -391,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { @@ -417,9 +419,9 @@ internal class DefaultCrossSigningService @Inject constructor( // Special case when it's me, // I have to check that MSK -> USK -> SSK // and that MSK is trusted (i know the private key, or is signed by a trusted device) - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId)) + return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId)) } fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult { @@ -429,7 +431,7 @@ internal class DefaultCrossSigningService @Inject constructor( // val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) val myMasterKey = myCrossSigningInfo?.masterKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) // Is the master key trusted // 1) check if I know the private key @@ -453,7 +455,7 @@ internal class DefaultCrossSigningService @Inject constructor( olmPkSigning?.releaseSigning() } else { // Maybe it's signed by a locally trusted device? - myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> val potentialDeviceId = key.removePrefix("ed25519:") val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { @@ -475,14 +477,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val myUserKey = myCrossSigningInfo.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK") return UserTrustResult.KeyNotSigned(myUserKey) } @@ -498,14 +500,14 @@ internal class DefaultCrossSigningService @Inject constructor( } val mySSKey = myCrossSigningInfo.selfSigningKey() - ?: return UserTrustResult.CrossSigningNotConfigured(userId) + ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures - ?.get(userId) // Signatures made by me + ?.get(myUserId) // Signatures made by me ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK") return UserTrustResult.KeyNotSigned(mySSKey) } @@ -555,14 +557,14 @@ internal class DefaultCrossSigningService @Inject constructor( override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - Mark user $userId as trusted ") + Timber.d("## CrossSigning - Mark user $otherUserId as trusted ") // We should have this user keys val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() if (otherMasterKeys == null) { callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) return@launch @@ -586,16 +588,22 @@ internal class DefaultCrossSigningService @Inject constructor( } cryptoStore.setUserKeysAsTrusted(otherUserId, true) - // TODO update local copy with new signature directly here? kind of local echo of trust? - Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK") val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature)) .build() uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { this.executionThread = TaskThread.CRYPTO this.callback = callback }.executeBy(taskExecutor) + + // Local echo for device cross trust, to avoid having to wait for a notification of key change + cryptoStore.getUserDeviceList(otherUserId)?.forEach { device -> + val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } } } @@ -604,20 +612,20 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.markMyMasterKeyAsLocallyTrusted(true) checkSelfTrust() // re-verify all trusts - onUsersDeviceUpdate(listOf(userId)) + onUsersDeviceUpdate(listOf(myUserId)) } } override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { // This device should be yours - val device = cryptoStore.getUserDevice(userId, deviceId) + val device = cryptoStore.getUserDevice(myUserId, deviceId) if (device == null) { callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) return@launch } - val myKeys = getUserCrossSigningKeys(userId) + val myKeys = getUserCrossSigningKeys(myUserId) if (myKeys == null) { callback.onFailure(Throwable("CrossSigning is not setup for this account")) return@launch @@ -639,7 +647,7 @@ internal class DefaultCrossSigningService @Inject constructor( } val toUpload = device.copy( signatures = mapOf( - userId + myUserId to mapOf( "ed25519:$ssPubKey" to newSignature @@ -661,8 +669,8 @@ internal class DefaultCrossSigningService @Inject constructor( val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) - val myKeys = getUserCrossSigningKeys(userId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + val myKeys = getUserCrossSigningKeys(myUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -717,7 +725,7 @@ internal class DefaultCrossSigningService @Inject constructor( fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() - myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) @@ -805,7 +813,7 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) // If it's me, recheck trust of all users and devices? val users = ArrayList<String>() - if (otherUserId == userId && currentTrust != trusted) { + if (otherUserId == myUserId && currentTrust != trusted) { // notify key requester outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 6d845ec59e9d6728ba79d86c115a8c01c11758f5..fffc6707d73b6f80de58f3506f1b046fb4f5f867 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -161,6 +161,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses // i have all the new trusts, update DB trusts.forEach { val verified = it.value?.isVerified() == true + Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified") updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) } @@ -259,21 +260,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses cryptoRealm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() - ?.crossSigningKeys - ?.forEach { info -> - // optimization to avoid trigger updates when there is no change.. - if (info.trustLevelEntity?.isVerified() != verified) { - Timber.d("## CrossSigning - Trust change for $userId : $verified") - val level = info.trustLevelEntity - if (level == null) { - info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = verified - it.crossSignedVerified = verified + ?.let { userKeyInfo -> + userKeyInfo + .crossSigningKeys + .forEach { key -> + // optimization to avoid trigger updates when there is no change.. + if (key.trustLevelEntity?.isVerified() != verified) { + Timber.d("## CrossSigning - Trust change for $userId : $verified") + val level = key.trustLevelEntity + if (level == null) { + key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { + it.locallyVerified = verified + it.crossSignedVerified = verified + } + } else { + level.locallyVerified = verified + level.crossSignedVerified = verified + } + } } - } else { - level.locallyVerified = verified - level.crossSignedVerified = verified - } + if (verified) { + userKeyInfo.wasUserVerifiedOnce = true } } } @@ -299,8 +306,18 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true } + val resetTrust = listToCheck + .filter { userId -> + val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId) + crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true + } + return if (allTrustedUserIds.isEmpty()) { - RoomEncryptionTrustLevel.Default + if (resetTrust.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + RoomEncryptionTrustLevel.Warning + } } else { // If one of the verified user as an untrusted device -> warning // If all devices of all verified users are trusted -> green @@ -327,11 +344,15 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses if (hasWarning) { RoomEncryptionTrustLevel.Warning } else { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted + if (resetTrust.isEmpty()) { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } } else { - RoomEncryptionTrustLevel.Default + RoomEncryptionTrustLevel.Warning } } } @@ -344,7 +365,8 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 56eba2524901878d04092919be5ed1c8e67e3f77..21e33423657ec8e4f0973c63eea0b1777dc3a5e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState @@ -120,11 +121,26 @@ internal interface IMXCryptoStore { fun getRoomsListBlacklistUnverifiedDevices(): List<String> /** - * Updates the rooms ids list in which the messages are not encrypted for the unverified devices. + * A live status regarding sharing keys for unverified devices in this room. * - * @param roomIds the room ids list + * @return Live status */ - fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> + + /** + * Tell if unverified devices should be blacklisted when sending keys. + * + * @return true if should not send keys to unverified devices + */ + fun getBlockUnverifiedDevices(roomId: String): Boolean + + /** + * Define if encryption keys should be sent to unverified devices in this room. + * + * @param roomId the roomId + * @param block if true will not send keys to unverified devices + */ + fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) /** * Get the current keys backup version. @@ -516,6 +532,9 @@ internal interface IMXCryptoStore { fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> + fun getGlobalCryptoConfig(): GlobalCryptoConfig + fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> + fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 3b8fa4cacdee6d382bed60099502578f90133a7c..e97cf437c68836363a5e0d5ce2637d9703e740c3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -29,6 +29,7 @@ import io.realm.kotlin.where import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState @@ -445,6 +446,38 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getGlobalCryptoConfig(): GlobalCryptoConfig { + return doWithRealm(realmConfiguration) { realm -> + realm.where<CryptoMetadataEntity>().findFirst() + ?.let { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } ?: GlobalCryptoConfig(false, false, false) + } + } + + override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where<CryptoMetadataEntity>() + }, + { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) + } + } + override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") doRealmTransaction(realmConfiguration) { realm -> @@ -1053,25 +1086,6 @@ internal class RealmCryptoStore @Inject constructor( } ?: false } - override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) { - doRealmTransaction(realmConfiguration) { - // Reset all - it.where<CryptoRoomEntity>() - .findAll() - .forEach { room -> - room.blacklistUnverifiedDevices = false - } - - // Enable those in the list - it.where<CryptoRoomEntity>() - .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray()) - .findAll() - .forEach { room -> - room.blacklistUnverifiedDevices = true - } - } - } - override fun getRoomsListBlacklistUnverifiedDevices(): List<String> { return doWithRealm(realmConfiguration) { it.where<CryptoRoomEntity>() @@ -1083,6 +1097,37 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where<CryptoRoomEntity>() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + }, + { + it.blacklistUnverifiedDevices + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: false + } + } + + override fun getBlockUnverifiedDevices(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { realm -> + realm.where<CryptoRoomEntity>() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() + ?.blacklistUnverifiedDevices ?: false + } + } + + override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId) + ?.blacklistUnverifiedDevices = block + } + } + override fun getDeviceTrackingStatuses(): Map<String, Int> { return doWithRealm(realmConfiguration) { it.where<UserEntity>() @@ -1611,7 +1656,8 @@ internal class RealmCryptoStore @Inject constructor( userId = userId, crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { crossSigningKeysMapper.map(userId, it) - } + }, + wasTrustedOnce = xsignInfo.wasUserVerifiedOnce ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 426d50a54fc997c1398e1d25874974b23d2c8d3d..de2b74308dd278883fe4effe3682ba10d150d74a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -49,7 +50,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 18L, + schemaVersion = 19L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -77,5 +78,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 16) MigrateCryptoTo016(realm).perform() if (oldVersion < 17) MigrateCryptoTo017(realm).perform() if (oldVersion < 18) MigrateCryptoTo018(realm).perform() + if (oldVersion < 19) MigrateCryptoTo019(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d2eb60a600879508134c866f982897eb54e06e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt @@ -0,0 +1,59 @@ +/* + * 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.store.db.migration + +import io.realm.DynamicRealm +import io.realm.DynamicRealmObject +import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * This migration is adding support for trusted flags on megolm sessions. + * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to + * mark existing keys as safe. + * This migration can take long depending on the account + */ +internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 18) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CrossSigningInfoEntity") + ?.addField(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, Boolean::class.java) + ?.transform { dynamicObject -> + + val knowKeys = dynamicObject.getList(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`) + val msk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.MASTER.value) + } + val ssk = knowKeys.firstOrNull { + it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.SELF_SIGNING.value) + } + val isTrusted = isDynamicKeyInfoTrusted(msk?.get<DynamicRealmObject>(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) && + isDynamicKeyInfoTrusted(ssk?.get<DynamicRealmObject>(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) + + dynamicObject.setBoolean(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, isTrusted) + } + } + + private fun isDynamicKeyInfoTrusted(keyInfo: DynamicRealmObject?): Boolean { + if (keyInfo == null) return false + return !keyInfo.isNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && + !keyInfo.isNull(TrustLevelEntityFields.LOCALLY_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt index 5aba9bb9ba627b82efb756e7367a2a65a164288d..033b7662c53efeef468b8ffcc08aa597bdbc23bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class CrossSigningInfoEntity( @PrimaryKey var userId: String? = null, + var wasUserVerifiedOnce: Boolean = false, var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList() ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..6eb4d5b1042263b359c87a41481674ea967e2b26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import android.content.Context +import android.os.Build +import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import javax.inject.Inject + +class ComputeUserAgentUseCase @Inject constructor( + private val context: Context, +) { + + /** + * Create an user agent with the application version. + * Ex: Element/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + * + * @param flavorDescription the flavor description + */ + fun execute(flavorDescription: String): String { + val appPackageName = context.applicationContext.packageName + val pm = context.packageManager + + val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() } + ?.takeIf { + it.matches("\\A\\p{ASCII}*\\z".toRegex()) + } + ?: run { + // Use appPackageName instead of appName if appName is null or contains any non-ASCII character + appPackageName + } + val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION + + val deviceManufacturer = Build.MANUFACTURER + val deviceModel = Build.MODEL + val androidVersion = Build.VERSION.RELEASE + val deviceBuildId = Build.DISPLAY + val matrixSdkVersion = BuildConfig.SDK_VERSION + + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(deviceManufacturer) + append(" ") + append(deviceModel) + append("; ") + append("Android ") + append(androidVersion) + append("; ") + append(deviceBuildId) + append("; ") + append("Flavour ") + append(flavorDescription) + append("; ") + append("MatrixAndroidSdk2 ") + append(matrixSdkVersion) + append(")") + } + } + + companion object { + const val FALLBACK_APP_VERSION = "0.0.0" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt index 28d96dfce7363f2ed58402109467c959557bf862..4e8326127777b83bc203f1797ee843516ffbb0f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt @@ -16,73 +16,20 @@ package org.matrix.android.sdk.internal.network -import android.content.Context -import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.di.MatrixScope -import timber.log.Timber import javax.inject.Inject @MatrixScope internal class UserAgentHolder @Inject constructor( - private val context: Context, - matrixConfiguration: MatrixConfiguration + matrixConfiguration: MatrixConfiguration, + computeUserAgentUseCase: ComputeUserAgentUseCase, ) { var userAgent: String = "" private set init { - setApplicationFlavor(matrixConfiguration.applicationFlavor) - } - - /** - * Create an user agent with the application version. - * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) - * - * @param flavorDescription the flavor description - */ - private fun setApplicationFlavor(flavorDescription: String) { - var appName = "" - var appVersion = "" - - try { - val appPackageName = context.applicationContext.packageName - val pm = context.packageManager - val appInfo = pm.getApplicationInfo(appPackageName, 0) - appName = pm.getApplicationLabel(appInfo).toString() - - val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) - appVersion = pkgInfo.versionName ?: "" - - // Use appPackageName instead of appName if appName contains any non-ASCII character - if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { - appName = appPackageName - } - } catch (e: Exception) { - Timber.e(e, "## initUserAgent() : failed") - } - - val systemUserAgent = System.getProperty("http.agent") - - // cannot retrieve the application version - if (appName.isEmpty() || appVersion.isEmpty()) { - if (null == systemUserAgent) { - userAgent = "Java" + System.getProperty("java.version") - } - return - } - - // if there is no user agent or cannot parse it - if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) { - userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")") - } else { - // update - userAgent = appName + "/" + appVersion + " " + - systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) + - "; Flavour " + flavorDescription + - "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")" - } + userAgent = computeUserAgentUseCase.execute(matrixConfiguration.applicationFlavor) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt deleted file mode 100644 index 55363a725103e5cb93183c96cec1dc20aab8ec16..0000000000000000000000000000000000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ /dev/null @@ -1,235 +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.session.room.send.queue - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.auth.data.sessionId -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.isLimitExceededError -import org.matrix.android.sdk.api.failure.isTokenError -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import timber.log.Timber -import java.io.IOException -import java.util.Timer -import java.util.TimerTask -import java.util.concurrent.LinkedBlockingQueue -import javax.inject.Inject -import kotlin.concurrent.schedule - -/** - * A simple ever running thread unique for that session responsible of sending events in order. - * Each send is retried 3 times, if there is no network (e.g if cannot ping homeserver) it will wait and - * periodically test reachability before resume (does not count as a retry) - * - * If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted - */ -@Deprecated("You should know use EventSenderProcessorCoroutine instead") -@SessionScope -internal class EventSenderProcessorThread @Inject constructor( - private val cryptoService: CryptoService, - private val sessionParams: SessionParams, - private val queuedTaskFactory: QueuedTaskFactory, - private val taskExecutor: TaskExecutor, - private val memento: QueueMemento -) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor { - - private fun markAsManaged(task: QueuedTask) { - memento.track(task) - } - - private fun markAsFinished(task: QueuedTask) { - memento.unTrack(task) - } - - override fun onSessionStarted(session: Session) { - start() - } - - override fun onSessionStopped(session: Session) { - interrupt() - } - - override fun start() { - super.start() - // We should check for sending events not handled because app was killed - // But we should be careful of only took those that was submitted to us, because if it's - // for example it's a media event it is handled by some worker and he will handle it - // This is a bit fragile :/ - // also some events cannot be retried manually by users, e.g reactions - // they were previously relying on workers to do the work :/ and was expected to always finally succeed - // Also some echos are not to be resent like redaction echos (fake event created for aggregation) - - tryOrNull { - taskExecutor.executorScope.launch { - Timber.d("## Send relaunched pending events on restart") - memento.restoreTasks(this@EventSenderProcessorThread) - } - } - } - - // API - override fun postEvent(event: Event): Cancelable { - return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false) - } - - override fun postEvent(event: Event, encrypt: Boolean): Cancelable { - val task = queuedTaskFactory.createSendTask(event, encrypt) - return postTask(task) - } - - override fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable { - return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason) - } - - override fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable { - val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason) - return postTask(task) - } - - override fun postTask(task: QueuedTask): Cancelable { - // non blocking add to queue - sendingQueue.add(task) - markAsManaged(task) - return task - } - - override fun cancel(eventId: String, roomId: String) { - (currentTask as? SendEventQueuedTask) - ?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId } - ?.cancel() - } - - companion object { - private const val RETRY_WAIT_TIME_MS = 10_000L - } - - private var currentTask: QueuedTask? = null - - private var sendingQueue = LinkedBlockingQueue<QueuedTask>() - - private var networkAvailableLock = Object() - private var canReachServer = true - private var retryNoNetworkTask: TimerTask? = null - - override fun run() { - Timber.v("## SendThread started") - try { - while (!isInterrupted) { - Timber.v("## SendThread wait for task to process") - val task = sendingQueue.take() - .also { currentTask = it } - Timber.v("## SendThread Found task to process $task") - - if (task.isCancelled()) { - Timber.v("## SendThread send cancelled for $task") - // we do not execute this one - continue - } - // we check for network connectivity - while (!canReachServer) { - Timber.v("## SendThread cannot reach server") - // schedule to retry - waitForNetwork() - // if thread as been killed meanwhile -// if (state == State.KILLING) break - } - Timber.v("## Server is Reachable") - // so network is available - - runBlocking { - retryLoop@ while (task.retryCount.get() < 3) { - try { - // SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER) - Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}") - task.execute() - // sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService)) - // SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER) - break@retryLoop - } catch (exception: Throwable) { - when { - exception is IOException || exception is Failure.NetworkConnection -> { - canReachServer = false - if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() - while (!canReachServer) { - Timber.v("## SendThread retryLoop cannot reach server") - // schedule to retry - waitForNetwork() - } - } - (exception.isLimitExceededError()) -> { - if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() - Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}") - // wait a bit - // Todo if its a quota exception can we get timout? - sleep(3_000) - continue@retryLoop - } - exception.isTokenError() -> { - Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt") - // we can exit the loop - task.onTaskFailed() - throw InterruptedException() - } - exception is CancellationException -> { - Timber.v("## SendThread task has been cancelled") - break@retryLoop - } - else -> { - Timber.v("## SendThread retryLoop Un-Retryable error, try next task") - // this task is in error, check next one? - task.onTaskFailed() - break@retryLoop - } - } - } - } - } - markAsFinished(task) - } - } catch (interruptionException: InterruptedException) { - // will be thrown is thread is interrupted while seeping - interrupt() - Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}") - } -// state = State.KILLED - // is this needed? - retryNoNetworkTask?.cancel() - Timber.w("## SendThread finished") - } - - private fun waitForNetwork() { - retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) { - synchronized(networkAvailableLock) { - canReachServer = HomeServerAvailabilityChecker(sessionParams).check().also { - Timber.v("## SendThread checkHostAvailable $it") - } - networkAvailableLock.notify() - } - } - synchronized(networkAvailableLock) { networkAvailableLock.wait() } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index b47b2156552ad344ce24d2c14f34b6d883f92d0b..d3f2a3f044925be19ef70e9a7bac51eed622e841 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError @@ -52,7 +53,6 @@ import javax.inject.Inject import kotlin.concurrent.schedule private const val RETRY_WAIT_TIME_MS = 10_000L -private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC) @@ -61,7 +61,8 @@ internal class SyncThread @Inject constructor( private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler, - private val lightweightSettingsStorage: DefaultLightweightSettingsStorage + private val lightweightSettingsStorage: DefaultLightweightSettingsStorage, + private val matrixConfiguration: MatrixConfiguration, ) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle @@ -181,7 +182,7 @@ internal class SyncThread @Inject constructor( val timeout = when { previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ afterPause -> 0L /* No timeout after a pause */ - else -> DEFAULT_LONG_POOL_TIMEOUT + else -> matrixConfiguration.syncConfig.longPollTimeout } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") val presence = lightweightSettingsStorage.getSyncPresenceStatus() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt index 950093760a11b874950f06357d320a590a454653..5b41ff6da07a54cd03592a33da19eb20864d3496 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt @@ -53,8 +53,10 @@ class UnRequestedKeysManagerTest { ), signatures = mapOf( aliceMxId to mapOf( - "ed25519:$device1Id" to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", - "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" + "ed25519:$device1Id" + to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", + "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" + to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" ) ), unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ed6f28d7e22cd5bae3436db318fff951b0ccacc --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.network + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.BuildConfig +import java.lang.Exception + +private const val A_PACKAGE_NAME = "org.matrix.sdk" +private const val AN_APP_NAME = "Element" +private const val A_NON_ASCII_APP_NAME = "Élement" +private const val AN_APP_VERSION = "1.5.1" +private const val A_FLAVOUR = "GooglePlay" + +class ComputeUserAgentUseCaseTest { + + private val context = mockk<Context>() + private val packageManager = mockk<PackageManager>() + private val applicationInfo = mockk<ApplicationInfo>() + private val packageInfo = mockk<PackageInfo>() + + private val computeUserAgentUseCase = ComputeUserAgentUseCase(context) + + @Before + fun setUp() { + every { context.applicationContext } returns context + every { context.packageName } returns A_PACKAGE_NAME + every { context.packageManager } returns packageManager + every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo + every { packageManager.getPackageInfo(any<String>(), any()) } returns packageInfo + } + + @Test + fun `given a non-null app name and app version when computing user agent then returns expected user agent`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(null) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a non-ascii app name when computing user agent then returns user agent with package name instead of app name`() { + // Given + givenAppName(A_NON_ASCII_APP_NAME) + givenAppVersion(AN_APP_VERSION) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + @Test + fun `given a null app version when computing user agent then returns user agent with a fallback app version`() { + // Given + givenAppName(AN_APP_NAME) + givenAppVersion(null) + + // When + val result = computeUserAgentUseCase.execute(A_FLAVOUR) + + // Then + val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, ComputeUserAgentUseCase.FALLBACK_APP_VERSION) + result shouldBeEqualTo expectedUserAgent + } + + private fun constructExpectedUserAgent(appName: String, appVersion: String): String { + return buildString { + append(appName) + append("/") + append(appVersion) + append(" (") + append(Build.MANUFACTURER) + append(" ") + append(Build.MODEL) + append("; ") + append("Android ") + append(Build.VERSION.RELEASE) + append("; ") + append(Build.DISPLAY) + append("; ") + append("Flavour ") + append(A_FLAVOUR) + append("; ") + append("MatrixAndroidSdk2 ") + append(BuildConfig.SDK_VERSION) + append(")") + } + } + + private fun givenAppName(deviceName: String?) { + if (deviceName == null) { + every { packageManager.getApplicationLabel(any()) } throws Exception("Cannot retrieve application name") + } else if (!deviceName.matches("\\A\\p{ASCII}*\\z".toRegex())) { + every { packageManager.getApplicationLabel(any()) } returns A_PACKAGE_NAME + } else { + every { packageManager.getApplicationLabel(any()) } returns deviceName + } + } + + private fun givenAppVersion(appVersion: String?) { + packageInfo.versionName = appVersion + } +}