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 36bf164c141..7110bbaff1e 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 @@ -1,804 +1,807 @@ -/* - * 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 android.util.Log -import androidx.test.filters.LargeTest -import kotlinx.coroutines.delay -import org.amshove.kluent.fail -import org.amshove.kluent.internal.assertEquals -import org.junit.Assert -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.Session -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.RequestResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestMatrixCallback -import java.util.concurrent.CountDownLatch - -@RunWith(JUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -@LargeTest -class E2eeSanityTests : InstrumentedTest { - - /** - * Simple test that create an e2ee room. - * Some new members are added, and a message is sent. - * We check that the message is e2e and can be decrypted. - * - * Additional users join, we check that they can't decrypt history - * - * Alice sends a new message, then check that the new one can be decrypted - */ - @Test - fun testSendingE2EEMessages() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val aliceSession = cryptoTestData.firstSession - val e2eRoomID = cryptoTestData.roomId - - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - // we want to disable key gossiping to just check initial sending of keys - aliceSession.cryptoService().enableKeyGossiping(false) - cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) - - // add some more users and invite them - val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)).also { - it.cryptoService().enableKeyGossiping(false) - } - } - - 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.invite(it.myUserId) - } - } - - // All user should accept invite - otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) - Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") - } - - // check that alice see them as joined (not really necessary?) - ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID) - - Log.v("#E2E TEST", "All users have joined the room") - Log.v("#E2E TEST", "Alice is sending the message") - - val text = "This is my message" - val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) - // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() - Assert.assertTrue("Message should be sent", sentEventId != null) - - // 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 - } - } - } - - // Add a new user to the room, and check that he can't decrypt - val newAccount = listOf("adam") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)) - } - - newAccount.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.invite(it.myUserId) - } - } - - newAccount.forEach { - waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) - } - - ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) - - // wait a bit - testHelper.runBlockingTest { - 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 - } - } - } - - // Let alice send a new message - Log.v("#E2E TEST", "Alice sends a new message") - - val secondMessage = "2 This is my message" - val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) - - // 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()?.body - } - } - } - - otherAccounts.forEach { - testHelper.signOutAndClose(it) - } - newAccount.forEach { testHelper.signOutAndClose(it) } - - cryptoTestData.cleanUp(testHelper) - } - - @Test - fun testKeyGossipingIsEnabledByDefault() { - val testHelper = CommonTestHelper(context()) - val session = testHelper.createAccount("alice", SessionTestParams(true)) - Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled()) - testHelper.signOutAndClose(session) - } - - /** - * Quick test for basic key backup - * 1. Create e2e between Alice and Bob - * 2. Alice sends 3 messages, using 3 different sessions - * 3. Ensure bob can decrypt - * 4. Create backup for bob and upload keys - * - * 5. Sign out alice and bob to ensure no gossiping will happen - * - * 6. Let bob sign in with a new session - * 7. Ensure history is UISI - * 8. Import backup - * 9. Check that new session can decrypt - */ - @Test - fun testBasicBackupImport() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - val e2eRoomID = cryptoTestData.roomId - - Log.v("#E2E TEST", "Create and start key backup for bob ...") - val bobKeysBackupService = bobSession.cryptoService().keysBackupService() - val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = testHelper.doSync { - bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) - } - val version = testHelper.doSync { - bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) - } - Log.v("#E2E TEST", "... Key backup started and enabled for bob") - // Bob session should now have - - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - - // let's send a few message to bob - val sentEventIds = mutableListOf() - val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") - messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { - 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 - } - } - // 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) - ) - } - 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 - - val bobUserId = bobSession.myUserId - Log.v("#E2E TEST", "Logout alice and bob...") - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) - Log.v("#E2E TEST", "..Logout alice and bob...") - - testHelper.runBlockingTest { - delay(1_000) - } - - // Create a new session for bob - Log.v("#E2E TEST", "Create a new session for Bob") - val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - - // 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 - } - } - } - // after initial sync events are not decrypted, so we have to try manually - cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) - - // Let's now import keys from backup - - newBobSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = testHelper.doSync { - kbs.getVersion(version.version, it) - } - - val importedResult = testHelper.doSync { - kbs.restoreKeyBackupWithPassword(keyVersionResult!!, - keyBackupPassword, - null, - null, - null, it) - } - - assertEquals(3, importedResult.totalNumberOfKeys) - } - - // ensure bob can now decrypt - cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) - - testHelper.signOutAndClose(newBobSession) - } - - /** - * Check that a new verified session that was not supposed to get the keys initially will - * get them from an older one. - */ - @Test - fun testSimpleGossip() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - val e2eRoomID = cryptoTestData.roomId - - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - - // let's send a few message to bob - val sentEventIds = mutableListOf() - val messagesText = listOf("1. Hello", "2. Bob") - - Log.v("#E2E TEST", "Alice sends some messages") - messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { - 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 - } - } - } - - // Ensure bob can decrypt - ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID) - - // Let's now add a new bob session - // Create a new session for bob - Log.v("#E2E TEST", "Create a new session for Bob") - val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - - // check that new bob can't currently decrypt - Log.v("#E2E TEST", "check that new bob can't currently decrypt") - - cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) -// newBobSession.cryptoService().getOutgoingRoomKeyRequests() -// .firstOrNull { -// it.sessionId == -// } - - // Try to request - sentEventIds.forEach { sentEventId -> - val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - newBobSession.cryptoService().requestRoomKeyForEvent(event) - } - - // wait a bit - // we need to wait a couple of syncs to let sharing occurs -// testHelper.waitFewSyncs(newBobSession, 6) - - // Ensure that new bob still can't decrypt (keys must have been withheld) - sentEventIds.forEach { sentEventId -> - val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! - .getTimelineEvent(sentEventId)!! - .root.content.toModel()!!.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 - } - } - } - - cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) - - // Now mark new bob session as verified - - bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) - newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!) - - // now let new session re-request - sentEventIds.forEach { sentEventId -> - val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - newBobSession.cryptoService().reRequestRoomKeyForEvent(event) - } - - cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) - - cryptoTestData.cleanUp(testHelper) - testHelper.signOutAndClose(newBobSession) - } - - /** - * Test that if a better key is forwarded (lower index, it is then used) - */ - @Test - fun testForwardBetterKey() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val aliceSession = cryptoTestData.firstSession - val bobSessionWithBetterKey = cryptoTestData.secondSession!! - val e2eRoomID = cryptoTestData.roomId - - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - - // let's send a few message to bob - var firstEventId: String - val firstMessage = "1. Hello" - - Log.v("#E2E TEST", "Alice sends some messages") - 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 - } - } - } - - // Ensure bob can decrypt - ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) - - // Let's add a new unverified session from bob - val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) - - // check that new bob can't currently decrypt - Log.v("#E2E TEST", "check that new bob can't currently decrypt") - cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null) - - // Now let alice send a new message. this time the new bob session will be able to decrypt - var secondEventId: String - val secondMessage = "2. New Device?" - - Log.v("#E2E TEST", "Alice sends some messages") - 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 - } - } - } - - // check that both messages have same sessionId (it's just that we don't have index 0) - val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) - val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) - - val firstSessionId = firstEventNewBobPov!!.root.content.toModel()!!.sessionId!! - val secondSessionId = secondEventNewBobPov!!.root.content.toModel()!!.sessionId!! - - Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) - - // Confirm we can decrypt one but not the other - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - fail("Should not be able to decrypt event") - } catch (_: MXCryptoError) { - } - } - - testHelper.runBlockingTest { - 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 - bobSessionWithBetterKey.cryptoService() - .verificationService() - .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) - - newBobSession.cryptoService() - .verificationService() - .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) - - // now let new session request - newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root) - - // We need to wait for the key request to be sent out and then a reply to be received - - // 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.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSessionWithBetterKey) - testHelper.signOutAndClose(newBobSession) - } - - private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendTextMessage(text) - var sentEventId: String? = null - testHelper.waitWithLatch(4 * 60_000L) { latch -> - val timeline = aliceRoomPOV.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()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } - - timeline.dispose() - } - return sentEventId - } - - /** - * Test that if a better key is forwared (lower index, it is then used) - */ - @Test - fun testSelfInteractiveVerificationAndGossip() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) - cryptoTestHelper.bootstrapSecurity(aliceSession) - - // now let's create a new login from alice - - 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 - } - } - }) - - // 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) - assertEquals("Decimal code should have matched", oldCode, newCode) - - // Assert that devices are verified - val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) - val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(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.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null - } - } - - assertEquals( - "MSK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master - ) - assertEquals( - "USK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user) - - assertEquals( - "SSK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned - ) - - // Let's check that we have the megolm backup key - assertEquals( - "Megolm key should be the same", - aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey - ) - assertEquals( - "Megolm version should be the same", - aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version - ) - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(aliceNewSession) - } - - private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } - } - } - } - - private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } - } - } - } - - // not sure why it's taking so long :/ - testHelper.runBlockingTest(90_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } - } - - Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } - } - } - - private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, 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 - } - } - } - } -} +/* + * 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 android.util.Log +import androidx.test.filters.LargeTest +import kotlinx.coroutines.delay +import org.amshove.kluent.fail +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +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.Session +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.RequestResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestMatrixCallback +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeSanityTests : InstrumentedTest { + + /** + * Simple test that create an e2ee room. + * Some new members are added, and a message is sent. + * We check that the message is e2e and can be decrypted. + * + * Additional users join, we check that they can't decrypt history + * + * Alice sends a new message, then check that the new one can be decrypted + */ + @Test + fun testSendingE2EEMessages() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + // we want to disable key gossiping to just check initial sending of keys + aliceSession.cryptoService().enableKeyGossiping(false) + cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) + + // add some more users and invite them + val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)).also { + it.cryptoService().enableKeyGossiping(false) + } + } + + 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.invite(it.myUserId) + } + } + + // All user should accept invite + otherAccounts.forEach { otherSession -> + waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) + Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") + } + + // check that alice see them as joined (not really necessary?) + ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID) + + Log.v("#E2E TEST", "All users have joined the room") + Log.v("#E2E TEST", "Alice is sending the message") + + val text = "This is my message" + val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) + // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() + Assert.assertTrue("Message should be sent", sentEventId != null) + + // 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 + } + } + } + + // Add a new user to the room, and check that he can't decrypt + val newAccount = listOf("adam") // , "adam", "manu") + .map { + testHelper.createAccount(it, SessionTestParams(true)) + } + + newAccount.forEach { + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.invite(it.myUserId) + } + } + + newAccount.forEach { + waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) + } + + ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) + + // wait a bit + testHelper.runBlockingTest { + 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 + } + } + } + + // Let alice send a new message + Log.v("#E2E TEST", "Alice sends a new message") + + val secondMessage = "2 This is my message" + val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) + + // 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()?.body + } + } + } + + otherAccounts.forEach { + testHelper.signOutAndClose(it) + } + newAccount.forEach { testHelper.signOutAndClose(it) } + + cryptoTestData.cleanUp(testHelper) + } + + @Test + fun testKeyGossipingIsEnabledByDefault() { + val testHelper = CommonTestHelper(context()) + val session = testHelper.createAccount("alice", SessionTestParams(true)) + Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled()) + testHelper.signOutAndClose(session) + } + + /** + * Quick test for basic key backup + * 1. Create e2e between Alice and Bob + * 2. Alice sends 3 messages, using 3 different sessions + * 3. Ensure bob can decrypt + * 4. Create backup for bob and upload keys + * + * 5. Sign out alice and bob to ensure no gossiping will happen + * + * 6. Let bob sign in with a new session + * 7. Ensure history is UISI + * 8. Import backup + * 9. Check that new session can decrypt + */ + @Test + fun testBasicBackupImport() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + Log.v("#E2E TEST", "Create and start key backup for bob ...") + val bobKeysBackupService = bobSession.cryptoService().keysBackupService() + val keyBackupPassword = "FooBarBaz" + val megolmBackupCreationInfo = testHelper.doSync { + bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) + } + val version = testHelper.doSync { + bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + Log.v("#E2E TEST", "... Key backup started and enabled for bob") + // Bob session should now have + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // let's send a few message to bob + val sentEventIds = mutableListOf() + val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") + messagesText.forEach { text -> + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { + 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 + } + } + // 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) + ) + } + 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 + + val bobUserId = bobSession.myUserId + Log.v("#E2E TEST", "Logout alice and bob...") + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSession) + Log.v("#E2E TEST", "..Logout alice and bob...") + + testHelper.runBlockingTest { + delay(1_000) + } + + // Create a new session for bob + Log.v("#E2E TEST", "Create a new session for Bob") + val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true)) + + // 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 + } + } + } + // after initial sync events are not decrypted, so we have to try manually + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + + // Let's now import keys from backup + + newBobSession.cryptoService().keysBackupService().let { kbs -> + val keyVersionResult = testHelper.doSync { + kbs.getVersion(version.version, it) + } + + val importedResult = testHelper.doSync { + kbs.restoreKeyBackupWithPassword( + keyVersionResult!!, + keyBackupPassword, + null, + null, + null, it + ) + } + + assertEquals(3, importedResult.totalNumberOfKeys) + } + + // ensure bob can now decrypt + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + + testHelper.signOutAndClose(newBobSession) + } + + /** + * Check that a new verified session that was not supposed to get the keys initially will + * get them from an older one. + */ + @Test + fun testSimpleGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // let's send a few message to bob + val sentEventIds = mutableListOf() + val messagesText = listOf("1. Hello", "2. Bob") + + Log.v("#E2E TEST", "Alice sends some messages") + messagesText.forEach { text -> + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { + 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 + } + } + } + + // Ensure bob can decrypt + ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID) + + // Let's now add a new bob session + // Create a new session for bob + Log.v("#E2E TEST", "Create a new session for Bob") + val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + // check that new bob can't currently decrypt + Log.v("#E2E TEST", "check that new bob can't currently decrypt") + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) +// newBobSession.cryptoService().getOutgoingRoomKeyRequests() +// .firstOrNull { +// it.sessionId == +// } + + // Try to request + sentEventIds.forEach { sentEventId -> + val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root + newBobSession.cryptoService().requestRoomKeyForEvent(event) + } + + // wait a bit + // we need to wait a couple of syncs to let sharing occurs +// testHelper.waitFewSyncs(newBobSession, 6) + + // Ensure that new bob still can't decrypt (keys must have been withheld) + sentEventIds.forEach { sentEventId -> + val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! + .getTimelineEvent(sentEventId)!! + .root.content.toModel()!!.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 + } + } + } + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) + + // Now mark new bob session as verified + + bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) + newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!) + + // now let new session re-request + sentEventIds.forEach { sentEventId -> + val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root + newBobSession.cryptoService().reRequestRoomKeyForEvent(event) + } + + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + + cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(newBobSession) + } + + /** + * Test that if a better key is forwarded (lower index, it is then used) + */ + @Test + fun testForwardBetterKey() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSessionWithBetterKey = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + + // let's send a few message to bob + var firstEventId: String + val firstMessage = "1. Hello" + + Log.v("#E2E TEST", "Alice sends some messages") + 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 + } + } + } + + // Ensure bob can decrypt + ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) + + // Let's add a new unverified session from bob + val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) + + // check that new bob can't currently decrypt + Log.v("#E2E TEST", "check that new bob can't currently decrypt") + cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null) + + // Now let alice send a new message. this time the new bob session will be able to decrypt + var secondEventId: String + val secondMessage = "2. New Device?" + + Log.v("#E2E TEST", "Alice sends some messages") + 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 + } + } + } + + // check that both messages have same sessionId (it's just that we don't have index 0) + val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + + val firstSessionId = firstEventNewBobPov!!.root.content.toModel()!!.sessionId!! + val secondSessionId = secondEventNewBobPov!!.root.content.toModel()!!.sessionId!! + + Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) + + // Confirm we can decrypt one but not the other + testHelper.runBlockingTest { + try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + fail("Should not be able to decrypt event") + } catch (_: MXCryptoError) { + } + } + + testHelper.runBlockingTest { + 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 + bobSessionWithBetterKey.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) + + newBobSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) + + // now let new session request + newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root) + + // We need to wait for the key request to be sent out and then a reply to be received + + // 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.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSessionWithBetterKey) + testHelper.signOutAndClose(newBobSession) + } + + private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { + aliceRoomPOV.sendTextMessage(text) + var sentEventId: String? = null + testHelper.waitWithLatch(4 * 60_000L) { latch -> + val timeline = aliceRoomPOV.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()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null + } + + timeline.dispose() + } + return sentEventId + } + + /** + * Test that if a better key is forwared (lower index, it is then used) + */ + @Test + fun testSelfInteractiveVerificationAndGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) + cryptoTestHelper.bootstrapSecurity(aliceSession) + + // now let's create a new login from alice + + 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 + } + } + }) + + // 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) + assertEquals("Decimal code should have matched", oldCode, newCode) + + // Assert that devices are verified + val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(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.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null + } + } + + assertEquals( + "MSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master + ) + assertEquals( + "USK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user + ) + + assertEquals( + "SSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned + ) + + // Let's check that we have the megolm backup key + assertEquals( + "Megolm key should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey + ) + assertEquals( + "Megolm version should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version + ) + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(aliceNewSession) + } + + private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + otherAccounts.map { + aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + } + + private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") + } + } + } + } + + // not sure why it's taking so long :/ + testHelper.runBlockingTest(90_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } + + private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, 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 + } + } + } + } +} 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 4eabede8370..f57b7eedfdb 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 @@ -1,281 +1,289 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.gossiping - -import android.util.Log -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import org.junit.Assert -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.NoOpMatrixCallback -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.RequestResult -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.MockOkHttpInterceptor -import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestConstants - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -@LargeTest -class WithHeldTests : InstrumentedTest { - - @Test - fun test_WithHeldUnverifiedReason() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - // ============================= - // ARRANGE - // ============================= - - val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - val bobSession = testHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - - // Initialize cross signing on both - cryptoTestHelper.initializeCrossSigning(aliceSession) - cryptoTestHelper.initializeCrossSigning(bobSession) - - val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) - cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId) - - val roomAlicePOV = aliceSession.getRoom(roomId)!! - - val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - // ============================= - // ACT - // ============================= - - // Alice decide to not send to unverified sessions - aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) - - 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 - } - } - - val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! - - val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!! - // ============================= - // ASSERT - // ============================= - - // Bob should not be able to decrypt because the keys is withheld - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) - } - - // 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 - } - } - // 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 - } - } - - // Previous message should still be undecryptable (partially withheld session) - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) - } - - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) - testHelper.signOutAndClose(bobUnverifiedSession) - } - - @Test - fun test_WithHeldNoOlm() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - val aliceSession = testData.firstSession - val bobSession = testData.secondSession!! - val aliceInterceptor = testHelper.getTestInterceptor(aliceSession) - - // Simulate no OTK - aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( - "/keys/claim", - 200, - """ - { "one_time_keys" : {} } - """ - )) - Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") - - val roomAlicePov = aliceSession.getRoom(testData.roomId)!! - - 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 - } - } - - // Previous message should still be undecryptable (partially withheld session) - val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) - try { - // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") - } - Assert.fail("This session should not be able to decrypt") - } catch (failure: Throwable) { - val type = (failure as MXCryptoError.Base).errorType - 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) - } - - // Ensure that alice has marked the session to be shared with bob - val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! - val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) - - Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) - // Add a new device for bob - - aliceInterceptor.clearRules() - val bobSecondSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true)) - // send a second message - val secondMessageId = testHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId - - // 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 - } - } - - val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) - - Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) - - aliceInterceptor.clearRules() - testData.cleanUp(testHelper) - testHelper.signOutAndClose(bobSecondSession) - } - - @Test - fun test_WithHeldKeyRequest() { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - - val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - val aliceSession = testData.firstSession - val bobSession = testData.secondSession!! - - val roomAlicePov = aliceSession.getRoom(testData.roomId)!! - - val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId - - testHelper.signOutAndClose(bobSession) - - // Create a new session for bob - - val bobSecondSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - // initialize to force request keys if missing - cryptoTestHelper.initializeCrossSigning(bobSecondSession) - - // Trust bob second device from Alice POV - aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) - bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) - - 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, "") - } - } - } - sessionId = timeLineEvent?.root?.content?.toModel()?.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.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSecondSession) - } -} +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.gossiping + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.RequestResult +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.MockOkHttpInterceptor +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class WithHeldTests : InstrumentedTest { + + @Test + fun test_WithHeldUnverifiedReason() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + // ============================= + // ARRANGE + // ============================= + + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = testHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) + + // Initialize cross signing on both + cryptoTestHelper.initializeCrossSigning(aliceSession) + cryptoTestHelper.initializeCrossSigning(bobSession) + + val roomId = cryptoTestHelper.createDM(aliceSession, bobSession) + cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId) + + val roomAlicePOV = aliceSession.getRoom(roomId)!! + + val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // ============================= + // ACT + // ============================= + + // Alice decide to not send to unverified sessions + aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) + + 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 + } + } + + val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! + + val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!! + // ============================= + // ASSERT + // ============================= + + // Bob should not be able to decrypt because the keys is withheld + try { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) + } + + // 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 + } + } + // 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 + } + } + + // Previous message should still be undecryptable (partially withheld session) + try { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") + } + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) + } + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSession) + testHelper.signOutAndClose(bobUnverifiedSession) + } + + @Test + fun test_WithHeldNoOlm() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val aliceInterceptor = testHelper.getTestInterceptor(aliceSession) + + // Simulate no OTK + aliceInterceptor!!.addRule( + MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ + { "one_time_keys" : {} } + """ + ) + ) + Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + 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 + } + } + + // Previous message should still be undecryptable (partially withheld session) + val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) + try { + // .. might need to wait a bit for stability? + testHelper.runBlockingTest { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") + } + Assert.fail("This session should not be able to decrypt") + } catch (failure: Throwable) { + val type = (failure as MXCryptoError.Base).errorType + 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) + } + + // Ensure that alice has marked the session to be shared with bob + val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( + bobSession.myUserId, + bobSession.sessionParams.credentials.deviceId + ) + + Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) + // Add a new device for bob + + aliceInterceptor.clearRules() + val bobSecondSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true)) + // send a second message + val secondMessageId = testHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId + + // 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 + } + } + + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( + bobSecondSession.myUserId, + bobSecondSession.sessionParams.credentials.deviceId + ) + + Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) + + aliceInterceptor.clearRules() + testData.cleanUp(testHelper) + testHelper.signOutAndClose(bobSecondSession) + } + + @Test + fun test_WithHeldKeyRequest() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + val roomAlicePov = aliceSession.getRoom(testData.roomId)!! + + val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + + testHelper.signOutAndClose(bobSession) + + // Create a new session for bob + + val bobSecondSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // initialize to force request keys if missing + cryptoTestHelper.initializeCrossSigning(bobSecondSession) + + // Trust bob second device from Alice POV + aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + + 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, "") + } + } + } + sessionId = timeLineEvent?.root?.content?.toModel()?.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.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSecondSession) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt index e1d75987670..8ec2932a8f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto.store.db.migration import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { @@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) realm.schema.remove("IncomingRoomKeyRequestEntity") // Not need to migrate existing request, just start fresh? - realm.schema.create("GossipingEventEntity") - .addField(GossipingEventEntityFields.TYPE, String::class.java) - .addIndex(GossipingEventEntityFields.TYPE) - .addField(GossipingEventEntityFields.CONTENT, String::class.java) - .addField(GossipingEventEntityFields.SENDER, String::class.java) - .addIndex(GossipingEventEntityFields.SENDER) - .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) - .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) - .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) - .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) - .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + .addField("type", String::class.java) + .addIndex("type") + .addField("content", String::class.java) + .addField("sender", String::class.java) + .addIndex("sender") + .addField("decryptionResultJson", String::class.java) + .addField("decryptionErrorCode", String::class.java) + .addField("ageLocalTs", Long::class.java) + .setNullable("ageLocalTs", true) + .addField("sendStateStr", String::class.java) realm.schema.create("IncomingGossipingRequestEntity") - .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) - .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) - .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) - .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("otherUserId", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("otherDeviceId", String::class.java) + .addField("requestStateStr", String::class.java) + .addField("localCreationTimestamp", Long::class.java) + .setNullable("localCreationTimestamp", true) realm.schema.create("OutgoingGossipingRequestEntity") - .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) - .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) - .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("recipientsData", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("requestStateStr", String::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt index 8d79ba30751..edc8b566adb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEnti import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator -internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 15) { +internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) { override fun doMigrate(realm: DynamicRealm) { realm.schema.remove("OutgoingGossipingRequestEntity") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt deleted file mode 100644 index 31b141014bc..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonDataException -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -import timber.log.Timber - -/** - * Keep track of gossiping event received in toDevice messages - * (room key request, or sss secret sharing, as well as cancellations) - * - */ - -// not used anymore, just here for db migration -internal open class GossipingEventEntity(@Index var type: String? = "", - var content: String? = null, - @Index var sender: String? = null, - var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null, - var ageLocalTs: Long? = null) : RealmObject() { - - private var sendStateStr: String = SendState.UNKNOWN.name - - var sendState: SendState - get() { - return SendState.valueOf(sendStateStr) - } - set(value) { - sendStateStr = value.name - } - - companion object - - fun setDecryptionResult(result: MXEventDecryptionResult) { - val decryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) - decryptionResultJson = adapter.toJson(decryptionResult) - decryptionErrorCode = null - } - - fun toModel(): Event { - return Event( - type = this.type ?: "", - content = ContentMapper.map(this.content), - senderId = this.sender - ).also { - it.ageLocalTs = this.ageLocalTs - it.sendState = this.sendState - this.decryptionResultJson?.let { json -> - try { - it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) - } catch (t: JsonDataException) { - Timber.e(t, "Failed to parse decryption result") - } - } - // TODO get the full crypto error object - it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> - MXCryptoError.ErrorType.valueOf(errorCode) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt deleted file mode 100644 index 5ac659d327d..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import io.realm.RealmObject -import io.realm.annotations.Index - -// not used anymore, just here for db migration -internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", - @Index var typeStr: String? = null, - var otherUserId: String? = null, - var requestedInfoStr: String? = null, - var otherDeviceId: String? = null, - var localCreationTimestamp: Long? = null -) : RealmObject() { - - private var requestStateStr: String = "" - - companion object -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt deleted file mode 100644 index 1dd9cacb74e..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ /dev/null @@ -1,31 +0,0 @@ - /* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import io.realm.RealmObject -import io.realm.annotations.Index - -// not used anymore, just here for db migration -internal open class OutgoingGossipingRequestEntity( - @Index var requestId: String? = null, - var recipientsData: String? = null, - var requestedInfoStr: String? = null, - @Index var typeStr: String? = null -) : RealmObject() { - - private var requestStateStr: String = "" -} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 40f8139ed9f..ac00f5fc304 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -422,7 +422,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( private fun tentativeRestoreBackup(res: Map?) { // It's not a good idea to download the full backup, it might take very long // and use a lot of resources - // Just check that the ey is valid and store it, the backup will be used megolm session per + // Just check that the key is valid and store it, the backup will be used megolm session per // megolm session when an UISI is encountered viewModelScope.launch(Dispatchers.IO) {