diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index 7e9a9e1b03..1f93d1feee 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -25,6 +25,7 @@
signup
ssss
threepid
+ unwedging
\ No newline at end of file
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
index 826c70a63f..1084dc423d 100644
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
+import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
@@ -40,7 +41,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
import java.util.HashMap
import java.util.concurrent.CountDownLatch
@@ -140,64 +140,38 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
* @return Alice, Bob and Sam session
*/
fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData {
- val statuses = HashMap()
-
val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val room = aliceSession.getRoom(aliceRoomId)!!
- val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
-
- val lock1 = CountDownLatch(2)
-
-// val samEventListener = object : MXEventListener() {
-// override fun onNewRoom(roomId: String) {
-// if (TextUtils.equals(roomId, aliceRoomId)) {
-// if (!statuses.containsKey("onNewRoom")) {
-// statuses["onNewRoom"] = "onNewRoom"
-// lock1.countDown()
-// }
-// }
-// }
-// }
-//
-// samSession.dataHandler.addListener(samEventListener)
-
- room.invite(samSession.myUserId, null, object : TestMatrixCallback(lock1) {
- override fun onSuccess(data: Unit) {
- statuses["invite"] = "invite"
- super.onSuccess(data)
- }
- })
-
- mTestHelper.await(lock1)
-
- assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
-
-// samSession.dataHandler.removeListener(samEventListener)
-
- val lock2 = CountDownLatch(1)
-
- samSession.joinRoom(aliceRoomId, null, object : TestMatrixCallback(lock2) {
- override fun onSuccess(data: Unit) {
- statuses["joinRoom"] = "joinRoom"
- super.onSuccess(data)
- }
- })
-
- mTestHelper.await(lock2)
- assertTrue(statuses.containsKey("joinRoom"))
+ val samSession = createSamAccountAndInviteToTheRoom(room)
// wait the initial sync
SystemClock.sleep(1000)
-// samSession.dataHandler.removeListener(samEventListener)
-
return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession)
}
+ /**
+ * Create Sam account and invite him in the room. He will accept the invitation
+ * @Return Sam session
+ */
+ fun createSamAccountAndInviteToTheRoom(room: Room): Session {
+ val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
+
+ mTestHelper.doSync {
+ room.invite(samSession.myUserId, null, it)
+ }
+
+ mTestHelper.doSync {
+ samSession.joinRoom(room.roomId, null, it)
+ }
+
+ return samSession
+ }
+
/**
* @return Alice and Bob sessions
*/
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/UnwedgingTest.kt
new file mode 100644
index 0000000000..ce5873b451
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/UnwedgingTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.matrix.android.internal.crypto
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.room.timeline.Timeline
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.CryptoTestHelper
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import java.util.concurrent.CountDownLatch
+
+/**
+ * Ref:
+ * - https://github.com/matrix-org/matrix-doc/pull/1719
+ * - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages
+ * - https://github.com/matrix-org/matrix-js-sdk/pull/780
+ * - https://github.com/matrix-org/matrix-ios-sdk/pull/778
+ * - https://github.com/matrix-org/matrix-ios-sdk/pull/784
+ */
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class UnwedgingTest : InstrumentedTest {
+
+ private lateinit var messagesReceivedByBob: List
+ private val mTestHelper = CommonTestHelper(context())
+ private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
+
+ @Before
+ fun init() {
+ messagesReceivedByBob = emptyList()
+ }
+
+ /**
+ * - Alice & Bob in a e2e room
+ * - Alice sends a 1st message with a 1st megolm session
+ * - Store the olm session between A&B devices
+ * - Alice sends a 2nd message with a 2nd megolm session
+ * - Simulate Alice using a backup of her OS and make her crypto state like after the first message
+ * - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
+ *
+ * What Bob must see:
+ * -> No issue with the 2 first messages
+ * -> The third event must fail to decrypt at first because Bob the olm session is wedged
+ * -> This is automatically fixed after SDKs restarted the olm session
+ */
+ @Test
+ fun testUnwedging() {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+ val aliceSession = cryptoTestData.firstSession
+ val aliceRoomId = cryptoTestData.roomId
+ val bobSession = cryptoTestData.secondSession!!
+
+ val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
+
+ bobSession.cryptoService().setWarnOnUnknownDevices(false)
+
+ aliceSession.cryptoService().setWarnOnUnknownDevices(false)
+
+ val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
+ val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
+
+ val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
+ bobTimeline.start()
+
+ var latch = CountDownLatch(1)
+ var bobEventsListener = createEventListener(latch, 1)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ // - Alice sends a 1st message with a 1st megolm session
+ roomFromAlicePOV.sendTextMessage("First message")
+
+ // Wait for the message to be received by Bob
+ mTestHelper.await(latch)
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 1
+
+ // - Store the olm session between A&B devices
+ // Let us pickle our session with bob here so we can later unpickle it
+ // and wedge our session.
+ val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!)
+ sessionIdsForBob!!.size shouldBe 1
+ val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
+
+ // Sam join the room
+ val samSession = mCryptoTestHelper.createSamAccountAndInviteToTheRoom(roomFromAlicePOV)
+
+ latch = CountDownLatch(1)
+ bobEventsListener = createEventListener(latch, 2)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ // - Alice sends a 2nd message with a 2nd megolm session
+ roomFromAlicePOV.sendTextMessage("Second message")
+
+ // Wait for the message to be received by Bob
+ mTestHelper.await(latch)
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 2
+
+ // Let us wedge the session now. Set crypto state like after the first message
+ aliceCryptoStore.storeSession(olmSession, bobSession.cryptoService().getMyDevice().identityKey()!!)
+
+ latch = CountDownLatch(1)
+ bobEventsListener = createEventListener(latch, 3)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
+ roomFromAlicePOV.sendTextMessage("Third message")
+
+ // Wait for the message to be received by Bob
+ mTestHelper.await(latch)
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 3
+
+ messagesReceivedByBob[0].root.getClearType() shouldBeEqualTo EventType.ENCRYPTED
+ messagesReceivedByBob[1].root.getClearType() shouldBeEqualTo EventType.MESSAGE
+ messagesReceivedByBob[2].root.getClearType() shouldBeEqualTo EventType.MESSAGE
+
+ bobTimeline.dispose()
+
+ cryptoTestData.cleanUp(mTestHelper)
+ mTestHelper.signOutAndClose(samSession)
+ }
+
+ private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {
+ return object : Timeline.Listener {
+ override fun onTimelineFailure(throwable: Throwable) {
+ // noop
+ }
+
+ override fun onNewTimelineEvents(eventIds: List) {
+ // noop
+ }
+
+ override fun onTimelineUpdated(snapshot: List) {
+ messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
+
+ if (messagesReceivedByBob.size == expectedNumberOfMessages) {
+ latch.countDown()
+ }
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
index aceead8ea0..61a072ece6 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt
@@ -21,6 +21,7 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context
import android.os.Handler
import android.os.Looper
+import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
@@ -1192,4 +1193,11 @@ internal class DefaultCryptoService @Inject constructor(
override fun getGossipingEventsTrail(): List {
return cryptoStore.getGossipingEventsTrail()
}
+
+ /* ==========================================================================================
+ * For test only
+ * ========================================================================================== */
+
+ @VisibleForTesting
+ val cryptoStoreForTesting = cryptoStore
}