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/CHANGES.md b/CHANGES.md
index 820fab0798..e727452189 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,7 @@ Features ✨:
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
- Save media files to Gallery (#973)
+ - Account deactivation (with password only) (#35)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)
@@ -23,6 +24,8 @@ Improvements 🙌:
- Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
- Show a warning dialog if the text of the clicked link does not match the link target (#922)
+ - Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
+ - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
Bugfix 🐛:
- Fix summary notification staying after "mark as read"
@@ -37,6 +40,7 @@ Bugfix 🐛:
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
- RiotX now uses as many threads as it needs to do work and send messages (#1221)
- Fix issue with media path (#1227)
+ - Add user to direct chat by user id (#1065)
Translations 🗣:
-
diff --git a/docs/notifications.md b/docs/notifications.md
index 328eb86954..8efcb87bf3 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -38,10 +38,10 @@ When the client receives the new information, it immediately sends another reque
This effectively emulates a server push feature.
The HTTP long Polling can be fine tuned in the **SDK** using two parameters:
-* timout (Sync request timeout)
+* timeout (Sync request timeout)
* delay (Delay between each sync)
-**timeout** is a server paramter, defined by:
+**timeout** is a server parameter, defined by:
```
The maximum time to wait, in milliseconds, before returning this request.`
If no events (or other data) become available before this time elapses, the server will return a response with empty fields.
diff --git a/docs/signin.md b/docs/signin.md
index 245ea444f6..e7368137ae 100644
--- a/docs/signin.md
+++ b/docs/signin.md
@@ -57,7 +57,7 @@ We get credential (200)
```json
{
- "user_id": "@benoit0816:matrix.org",
+ "user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
@@ -128,6 +128,8 @@ We get the credentials (200)
}
```
+It's worth noting that the response from the homeserver contains the userId of Alice.
+
### Login with Msisdn
Not supported yet in RiotX
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt
new file mode 100644
index 0000000000..2574700d49
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.account
+
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.failure.isInvalidPassword
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.SessionTestParams
+import im.vector.matrix.android.common.TestConstants
+import org.amshove.kluent.shouldBeTrue
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class ChangePasswordTest : InstrumentedTest {
+
+ private val commonTestHelper = CommonTestHelper(context())
+
+ companion object {
+ private const val NEW_PASSWORD = "this is a new password"
+ }
+
+ @Test
+ fun changePasswordTest() {
+ val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
+
+ // Change password
+ commonTestHelper.doSync {
+ session.changePassword(TestConstants.PASSWORD, NEW_PASSWORD, it)
+ }
+
+ // Try to login with the previous password, it will fail
+ val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
+ throwable.isInvalidPassword().shouldBeTrue()
+
+ // Try to login with the new password, should work
+ val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
+
+ commonTestHelper.signOutAndClose(session)
+ commonTestHelper.signOutAndClose(session2)
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt
new file mode 100644
index 0000000000..17ff984bc8
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.account
+
+import im.vector.matrix.android.InstrumentedTest
+import im.vector.matrix.android.api.auth.data.LoginFlowResult
+import im.vector.matrix.android.api.auth.registration.RegistrationResult
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.SessionTestParams
+import im.vector.matrix.android.common.TestConstants
+import im.vector.matrix.android.common.TestMatrixCallback
+import org.junit.Assert.assertTrue
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class DeactivateAccountTest : InstrumentedTest {
+
+ private val commonTestHelper = CommonTestHelper(context())
+
+ @Test
+ fun deactivateAccountTest() {
+ val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
+
+ // Deactivate the account
+ commonTestHelper.doSync {
+ session.deactivateAccount(TestConstants.PASSWORD, false, it)
+ }
+
+ // Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
+ val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
+
+ // Test the error
+ assertTrue(throwable is Failure.ServerError
+ && throwable.error.code == MatrixError.M_USER_DEACTIVATED
+ && throwable.error.message == "This account has been deactivated")
+
+ // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE)
+ val hs = commonTestHelper.createHomeServerConfig()
+
+ commonTestHelper.doSync {
+ commonTestHelper.matrix.authenticationService.getLoginFlow(hs, it)
+ }
+
+ var accountCreationError: Throwable? = null
+ commonTestHelper.waitWithLatch {
+ commonTestHelper.matrix.authenticationService
+ .getRegistrationWizard()
+ .createAccount(session.myUserId.substringAfter("@").substringBefore(":"),
+ TestConstants.PASSWORD,
+ null,
+ object : TestMatrixCallback(it, false) {
+ override fun onFailure(failure: Throwable) {
+ accountCreationError = failure
+ super.onFailure(failure)
+ }
+ })
+ }
+
+ // Test the error
+ accountCreationError.let {
+ assertTrue(it is Failure.ServerError
+ && it.error.code == MatrixError.M_USER_IN_USE)
+ }
+
+ // No need to close the session, it has been deactivated
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt
index 3cf03fff53..d168fe8ea6 100644
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt
@@ -44,6 +44,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
+import timber.log.Timber
import java.util.ArrayList
import java.util.UUID
import java.util.concurrent.CountDownLatch
@@ -58,6 +59,8 @@ class CommonTestHelper(context: Context) {
val matrix: Matrix
init {
+ Timber.plant(Timber.DebugTree())
+
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
matrix = Matrix.getInstance(context)
@@ -183,9 +186,9 @@ class CommonTestHelper(context: Context) {
* @param testParams test params about the session
* @return the session associated with the existing account
*/
- private fun logIntoAccount(userId: String,
- password: String,
- testParams: SessionTestParams): Session {
+ fun logIntoAccount(userId: String,
+ password: String,
+ testParams: SessionTestParams): Session {
val session = logAccountAndSync(userId, password, testParams)
assertNotNull(session)
return session
@@ -260,14 +263,45 @@ class CommonTestHelper(context: Context) {
return session
}
+ /**
+ * Log into the account and expect an error
+ *
+ * @param userName the account username
+ * @param password the password
+ */
+ fun logAccountWithError(userName: String,
+ password: String): Throwable {
+ val hs = createHomeServerConfig()
+
+ doSync {
+ matrix.authenticationService
+ .getLoginFlow(hs, it)
+ }
+
+ var requestFailure: Throwable? = null
+ waitWithLatch { latch ->
+ matrix.authenticationService
+ .getLoginWizard()
+ .login(userName, password, "myDevice", object : TestMatrixCallback(latch, onlySuccessful = false) {
+ override fun onFailure(failure: Throwable) {
+ requestFailure = failure
+ super.onFailure(failure)
+ }
+ })
+ }
+
+ assertNotNull(requestFailure)
+ return requestFailure!!
+ }
+
/**
* Await for a latch and ensure the result is true
*
* @param latch
* @throws InterruptedException
*/
- fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
- assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
+ fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
+ assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
}
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
@@ -282,10 +316,10 @@ class CommonTestHelper(context: Context) {
}
}
- fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
+ fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
block(latch)
- await(latch, timout)
+ await(latch, timeout)
}
// Transform a method with a MatrixCallback to a synchronous method
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..9278bed918 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,8 +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
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@@ -140,64 +139,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..6391c0392c
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/UnwedgingTest.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.extensions.tryThis
+import im.vector.matrix.android.api.session.crypto.MXCryptoError
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.toModel
+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 im.vector.matrix.android.common.TestConstants
+import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
+import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
+import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
+import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
+import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
+import org.amshove.kluent.shouldBe
+import org.junit.Assert
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.matrix.olm.OlmSession
+import timber.log.Timber
+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()
+
+ val bobFinalLatch = CountDownLatch(1)
+ val bobHasThreeDecryptedEventsListener = object : Timeline.Listener {
+ override fun onTimelineFailure(throwable: Throwable) {
+ // noop
+ }
+
+ override fun onNewTimelineEvents(eventIds: List) {
+ // noop
+ }
+
+ override fun onTimelineUpdated(snapshot: List) {
+ val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
+ Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages")
+ if (decryptedEventReceivedByBob.size == 3) {
+ if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
+ bobFinalLatch.countDown()
+ }
+ }
+ }
+ }
+ bobTimeline.addListener(bobHasThreeDecryptedEventsListener)
+
+ var latch = CountDownLatch(1)
+ var bobEventsListener = createEventListener(latch, 1)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ // - Alice sends a 1st message with a 1st megolm session
+ roomFromAlicePOV.sendTextMessage("First message")
+
+ // Wait for the message to be received by Bob
+ mTestHelper.await(latch)
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 1
+ val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!!
+
+ // - Store the olm session between A&B devices
+ // Let us pickle our session with bob here so we can later unpickle it
+ // and wedge our session.
+ val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!)
+ sessionIdsForBob!!.size shouldBe 1
+ val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
+
+ val oldSession = serializeForRealm(olmSession.olmSession)
+
+ aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
+ Thread.sleep(6_000)
+
+ latch = CountDownLatch(1)
+ bobEventsListener = createEventListener(latch, 2)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
+ // - Alice sends a 2nd message with a 2nd megolm session
+ roomFromAlicePOV.sendTextMessage("Second message")
+
+ // Wait for the message to be received by Bob
+ mTestHelper.await(latch)
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 2
+ // Session should have changed
+ val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!!
+ Assert.assertNotEquals(firstMessageSession, secondMessageSession)
+
+ // Let us wedge the session now. Set crypto state like after the first message
+ Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
+
+ aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
+ Thread.sleep(6_000)
+
+ // Force new session, and key share
+ aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
+
+ // Wait for the message to be received by Bob
+ mTestHelper.waitWithLatch {
+ bobEventsListener = createEventListener(it, 3)
+ bobTimeline.addListener(bobEventsListener)
+ messagesReceivedByBob = emptyList()
+
+ Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
+ // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
+ roomFromAlicePOV.sendTextMessage("Third message")
+ // Bob should not be able to decrypt, because the session key could not be sent
+ }
+ bobTimeline.removeListener(bobEventsListener)
+
+ messagesReceivedByBob.size shouldBe 3
+
+ val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!!
+ Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
+ Assert.assertNotEquals(secondMessageSession, thirdMessageSession)
+
+ Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType())
+ Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
+ Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
+ // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
+ mTestHelper.await(bobFinalLatch)
+ bobTimeline.removeListener(bobHasThreeDecryptedEventsListener)
+
+ // It's a trick to force key request on fail to decrypt
+ mTestHelper.doSync {
+ bobSession.cryptoService().crossSigningService()
+ .initializeCrossSigning(UserPasswordAuth(
+ user = bobSession.myUserId,
+ password = TestConstants.PASSWORD
+ ), it)
+ }
+
+ // Wait until we received back the key
+ mTestHelper.waitWithLatch {
+ mTestHelper.retryPeriodicallyWithLatch(it) {
+ // we should get back the key and be able to decrypt
+ val result = tryThis {
+ bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+ }
+ Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
+ result != null
+ }
+ }
+
+ bobTimeline.dispose()
+
+ cryptoTestData.cleanUp(mTestHelper)
+ }
+
+ 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/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt
new file mode 100644
index 0000000000..f10f2fef0e
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keysbackup
+
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.CryptoTestData
+import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
+
+/**
+ * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
+ */
+data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
+ val aliceKeys: List,
+ val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
+ val aliceSession2: Session) {
+ fun cleanUp(testHelper: CommonTestHelper) {
+ cryptoTestData.cleanUp(testHelper)
+ testHelper.signOutAndClose(aliceSession2)
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt
index 3042a3c68f..59ef24beec 100644
--- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -20,27 +20,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.listeners.StepProgressListener
-import im.vector.matrix.android.api.session.Session
-import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import im.vector.matrix.android.common.CommonTestHelper
-import im.vector.matrix.android.common.CryptoTestData
import im.vector.matrix.android.common.CryptoTestHelper
-import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback
-import im.vector.matrix.android.common.assertDictEquals
-import im.vector.matrix.android.common.assertListEquals
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
-import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
-import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -61,9 +53,7 @@ class KeysBackupTest : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
-
- private val defaultSessionParams = SessionTestParams(withInitialSync = false)
- private val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true)
+ private val mKeysBackupTestHelper = KeysBackupTestHelper(mTestHelper, mCryptoTestHelper)
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
@@ -110,7 +100,7 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun prepareKeysBackupVersionTest() {
- val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
+ val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
assertNotNull(bobSession.cryptoService().keysBackupService())
@@ -139,7 +129,7 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun createKeysBackupVersionTest() {
- val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
+ val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
val keysBackup = bobSession.cryptoService().keysBackupService()
@@ -182,7 +172,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup, latch, 5)
- prepareAndCreateKeysBackupData(keysBackup)
+ mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
mTestHelper.await(latch)
@@ -216,7 +206,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
- prepareAndCreateKeysBackupData(keysBackup)
+ mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Check that backupAllGroupSessions returns valid data
val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)
@@ -263,7 +253,7 @@ class KeysBackupTest : InstrumentedTest {
// - Pick a megolm key
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
- val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
+ val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session)
@@ -281,7 +271,7 @@ class KeysBackupTest : InstrumentedTest {
decryption!!)
assertNotNull(sessionData)
// - Compare the decrypted megolm key with the original one
- assertKeysEquals(session.exportKeys(), sessionData)
+ mKeysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(mTestHelper)
@@ -295,7 +285,7 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun restoreKeysBackupTest() {
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver
val importRoomKeysResult = mTestHelper.doSync {
@@ -308,7 +298,7 @@ class KeysBackupTest : InstrumentedTest {
)
}
- checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
+ mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper)
}
@@ -329,7 +319,7 @@ class KeysBackupTest : InstrumentedTest {
// fun restoreKeysBackupAndKeyShareRequestTest() {
// fail("Check with Valere for this test. I think we do not send key share request")
//
-// val testData = createKeysBackupScenarioWithPassword(null)
+// val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
//
// // - Check the SDK sent key share requests
// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
@@ -352,7 +342,7 @@ class KeysBackupTest : InstrumentedTest {
// )
// }
//
-// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
+// mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
//
// // - There must be no more pending key share requests
// val unsentRequestAfterRestoration = cryptoStore2
@@ -380,7 +370,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionTest() {
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@@ -399,7 +389,7 @@ class KeysBackupTest : InstrumentedTest {
}
// Wait for backup state to be ReadyToBackUp
- waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+ mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@@ -439,7 +429,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionWithRecoveryKeyTest() {
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@@ -458,7 +448,7 @@ class KeysBackupTest : InstrumentedTest {
}
// Wait for backup state to be ReadyToBackUp
- waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+ mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@@ -496,7 +486,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@@ -539,7 +529,7 @@ class KeysBackupTest : InstrumentedTest {
// - Do an e2e backup to the homeserver with a password
// - And log Alice on a new device
- val testData = createKeysBackupScenarioWithPassword(password)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@@ -558,7 +548,7 @@ class KeysBackupTest : InstrumentedTest {
}
// Wait for backup state to be ReadyToBackUp
- waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
+ mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@@ -599,7 +589,7 @@ class KeysBackupTest : InstrumentedTest {
// - Do an e2e backup to the homeserver with a password
// - And log Alice on a new device
- val testData = createKeysBackupScenarioWithPassword(password)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@@ -634,7 +624,7 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a wrong recovery key
val latch2 = CountDownLatch(1)
@@ -669,7 +659,7 @@ class KeysBackupTest : InstrumentedTest {
fun testBackupWithPassword() {
val password = "password"
- val testData = createKeysBackupScenarioWithPassword(password)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the password
val steps = ArrayList()
@@ -709,7 +699,7 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress)
assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
- checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
+ mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper)
}
@@ -725,7 +715,7 @@ class KeysBackupTest : InstrumentedTest {
val password = "password"
val wrongPassword = "passw0rd"
- val testData = createKeysBackupScenarioWithPassword(password)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Try to restore the e2e backup with a wrong password
val latch2 = CountDownLatch(1)
@@ -760,7 +750,7 @@ class KeysBackupTest : InstrumentedTest {
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
val password = "password"
- val testData = createKeysBackupScenarioWithPassword(password)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the recovery key.
val importRoomKeysResult = mTestHelper.doSync {
@@ -773,7 +763,7 @@ class KeysBackupTest : InstrumentedTest {
)
}
- checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
+ mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper)
}
@@ -786,7 +776,7 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
- val testData = createKeysBackupScenarioWithPassword(null)
+ val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a password
val latch2 = CountDownLatch(1)
@@ -825,7 +815,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
// - Do an e2e backup to the homeserver
- prepareAndCreateKeysBackupData(keysBackup)
+ mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Get key backup version from the home server
val keysVersionResult = mTestHelper.doSync {
@@ -870,13 +860,13 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
- val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup)
+ val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled)
// - Restart alice session
// - Log Alice on a new device
- val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync)
+ val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
cryptoTestData.cleanUp(mTestHelper)
@@ -950,7 +940,7 @@ class KeysBackupTest : InstrumentedTest {
})
// - Make alice back up her keys to her homeserver
- prepareAndCreateKeysBackupData(keysBackup)
+ mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled)
@@ -1000,7 +990,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup)
// - Make alice back up her keys to her homeserver
- prepareAndCreateKeysBackupData(keysBackup)
+ mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Wait for keys backup to finish by asking again to backup keys.
mTestHelper.doSync {
@@ -1012,7 +1002,7 @@ class KeysBackupTest : InstrumentedTest {
val aliceUserId = cryptoTestData.firstSession.myUserId
// - Log Alice on a new device
- val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync)
+ val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
// - Post a message to have a new megolm session
aliceSession2.cryptoService().setWarnOnUnknownDevices(false)
@@ -1093,7 +1083,7 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
- val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup)
+ val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled)
@@ -1106,169 +1096,4 @@ class KeysBackupTest : InstrumentedTest {
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(mTestHelper)
}
-
- /* ==========================================================================================
- * Private
- * ========================================================================================== */
-
- /**
- * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
- * KeysBackup object to be in the specified state
- */
- private fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
- // If already in the wanted state, return
- if (session.cryptoService().keysBackupService().state == state) {
- return
- }
-
- // Else observe state changes
- val latch = CountDownLatch(1)
-
- session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener {
- override fun onStateChange(newState: KeysBackupState) {
- if (newState == state) {
- session.cryptoService().keysBackupService().removeListener(this)
- latch.countDown()
- }
- }
- })
-
- mTestHelper.await(latch)
- }
-
- private data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
- val version: String)
-
- private fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService,
- password: String? = null): PrepareKeysBackupDataResult {
- val stateObserver = StateObserver(keysBackup)
-
- val megolmBackupCreationInfo = mTestHelper.doSync {
- keysBackup.prepareKeysBackupVersion(password, null, it)
- }
-
- assertNotNull(megolmBackupCreationInfo)
-
- assertFalse(keysBackup.isEnabled)
-
- // Create the version
- val keysVersion = mTestHelper.doSync {
- keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
- }
-
- assertNotNull(keysVersion.version)
-
- // Backup must be enable now
- assertTrue(keysBackup.isEnabled)
-
- stateObserver.stopAndCheckStates(null)
- return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
- }
-
- private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
- assertNotNull(keys1)
- assertNotNull(keys2)
-
- assertEquals(keys1?.algorithm, keys2?.algorithm)
- assertEquals(keys1?.roomId, keys2?.roomId)
- // No need to compare the shortcut
- // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key)
- assertEquals(keys1?.senderKey, keys2?.senderKey)
- assertEquals(keys1?.sessionId, keys2?.sessionId)
- assertEquals(keys1?.sessionKey, keys2?.sessionKey)
-
- assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain)
- assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys)
- }
-
- /**
- * Data class to store result of [createKeysBackupScenarioWithPassword]
- */
- private data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
- val aliceKeys: List,
- val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
- val aliceSession2: Session) {
- fun cleanUp(testHelper: CommonTestHelper) {
- cryptoTestData.cleanUp(testHelper)
- testHelper.signOutAndClose(aliceSession2)
- }
- }
-
- /**
- * Common initial condition
- * - Do an e2e backup to the homeserver
- * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted)
- *
- * @param password optional password
- */
- private fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
- val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
-
- val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
- val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
-
- val stateObserver = StateObserver(keysBackup)
-
- val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
-
- // - Do an e2e backup to the homeserver
- val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
-
- var lastProgress = 0
- var lastTotal = 0
- mTestHelper.doSync {
- keysBackup.backupAllGroupSessions(object : ProgressListener {
- override fun onProgress(progress: Int, total: Int) {
- lastProgress = progress
- lastTotal = total
- }
- }, it)
- }
-
- assertEquals(2, lastProgress)
- assertEquals(2, lastTotal)
-
- val aliceUserId = cryptoTestData.firstSession.myUserId
-
- // - Log Alice on a new device
- val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync)
-
- // Test check: aliceSession2 has no keys at login
- assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
-
- // Wait for backup state to be NotTrusted
- waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
-
- stateObserver.stopAndCheckStates(null)
-
- return KeysBackupScenarioData(cryptoTestData,
- aliceKeys,
- prepareKeysBackupDataResult,
- aliceSession2)
- }
-
- /**
- * Common restore success check after [createKeysBackupScenarioWithPassword]:
- * - Imported keys number must be correct
- * - The new device must have the same count of megolm keys
- * - Alice must have the same keys on both devices
- */
- private fun checkRestoreSuccess(testData: KeysBackupScenarioData,
- total: Int,
- imported: Int) {
- // - Imported keys number must be correct
- assertEquals(testData.aliceKeys.size, total)
- assertEquals(total, imported)
-
- // - The new device must have the same count of megolm keys
- assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false))
-
- // - Alice must have the same keys on both devices
- for (aliceKey1 in testData.aliceKeys) {
- val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
- .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
- assertNotNull(aliceKey2)
- assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
- }
- }
}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestConstants.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestConstants.kt
new file mode 100644
index 0000000000..0f3a23df3f
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestConstants.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.keysbackup
+
+import im.vector.matrix.android.common.SessionTestParams
+
+object KeysBackupTestConstants {
+ val defaultSessionParams = SessionTestParams(withInitialSync = false)
+ val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true)
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestHelper.kt
new file mode 100644
index 0000000000..bb1436b8d4
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.keysbackup
+
+import im.vector.matrix.android.api.listeners.ProgressListener
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
+import im.vector.matrix.android.common.CommonTestHelper
+import im.vector.matrix.android.common.CryptoTestHelper
+import im.vector.matrix.android.common.assertDictEquals
+import im.vector.matrix.android.common.assertListEquals
+import im.vector.matrix.android.internal.crypto.MegolmSessionData
+import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
+import org.junit.Assert
+import java.util.concurrent.CountDownLatch
+
+class KeysBackupTestHelper(
+ private val mTestHelper: CommonTestHelper,
+ private val mCryptoTestHelper: CryptoTestHelper) {
+
+ /**
+ * Common initial condition
+ * - Do an e2e backup to the homeserver
+ * - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted)
+ *
+ * @param password optional password
+ */
+ fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
+ val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
+
+ val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
+ val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
+
+ val stateObserver = StateObserver(keysBackup)
+
+ val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
+
+ // - Do an e2e backup to the homeserver
+ val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
+
+ var lastProgress = 0
+ var lastTotal = 0
+ mTestHelper.doSync {
+ keysBackup.backupAllGroupSessions(object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ lastProgress = progress
+ lastTotal = total
+ }
+ }, it)
+ }
+
+ Assert.assertEquals(2, lastProgress)
+ Assert.assertEquals(2, lastTotal)
+
+ val aliceUserId = cryptoTestData.firstSession.myUserId
+
+ // - Log Alice on a new device
+ val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
+
+ // Test check: aliceSession2 has no keys at login
+ Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
+
+ // Wait for backup state to be NotTrusted
+ waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
+
+ stateObserver.stopAndCheckStates(null)
+
+ return KeysBackupScenarioData(cryptoTestData,
+ aliceKeys,
+ prepareKeysBackupDataResult,
+ aliceSession2)
+ }
+
+ fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService,
+ password: String? = null): PrepareKeysBackupDataResult {
+ val stateObserver = StateObserver(keysBackup)
+
+ val megolmBackupCreationInfo = mTestHelper.doSync {
+ keysBackup.prepareKeysBackupVersion(password, null, it)
+ }
+
+ Assert.assertNotNull(megolmBackupCreationInfo)
+
+ Assert.assertFalse(keysBackup.isEnabled)
+
+ // Create the version
+ val keysVersion = mTestHelper.doSync {
+ keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
+ }
+
+ Assert.assertNotNull(keysVersion.version)
+
+ // Backup must be enable now
+ Assert.assertTrue(keysBackup.isEnabled)
+
+ stateObserver.stopAndCheckStates(null)
+ return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
+ }
+
+ /**
+ * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
+ * KeysBackup object to be in the specified state
+ */
+ fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
+ // If already in the wanted state, return
+ if (session.cryptoService().keysBackupService().state == state) {
+ return
+ }
+
+ // Else observe state changes
+ val latch = CountDownLatch(1)
+
+ session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener {
+ override fun onStateChange(newState: KeysBackupState) {
+ if (newState == state) {
+ session.cryptoService().keysBackupService().removeListener(this)
+ latch.countDown()
+ }
+ }
+ })
+
+ mTestHelper.await(latch)
+ }
+
+ fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
+ Assert.assertNotNull(keys1)
+ Assert.assertNotNull(keys2)
+
+ Assert.assertEquals(keys1?.algorithm, keys2?.algorithm)
+ Assert.assertEquals(keys1?.roomId, keys2?.roomId)
+ // No need to compare the shortcut
+ // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key)
+ Assert.assertEquals(keys1?.senderKey, keys2?.senderKey)
+ Assert.assertEquals(keys1?.sessionId, keys2?.sessionId)
+ Assert.assertEquals(keys1?.sessionKey, keys2?.sessionKey)
+
+ assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain)
+ assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys)
+ }
+
+ /**
+ * Common restore success check after [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]:
+ * - Imported keys number must be correct
+ * - The new device must have the same count of megolm keys
+ * - Alice must have the same keys on both devices
+ */
+ fun checkRestoreSuccess(testData: KeysBackupScenarioData,
+ total: Int,
+ imported: Int) {
+ // - Imported keys number must be correct
+ Assert.assertEquals(testData.aliceKeys.size, total)
+ Assert.assertEquals(total, imported)
+
+ // - The new device must have the same count of megolm keys
+ Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false))
+
+ // - Alice must have the same keys on both devices
+ for (aliceKey1 in testData.aliceKeys) {
+ val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
+ .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
+ Assert.assertNotNull(aliceKey2)
+ assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt
new file mode 100644
index 0000000000..91d00cbe21
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.keysbackup
+
+import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+
+data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
+ val version: String)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt
index 68643ff723..ddbaaea6ef 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt
@@ -23,11 +23,28 @@ import im.vector.matrix.android.api.util.Cancelable
* This interface defines methods to manage the account. It's implemented at the session level.
*/
interface AccountService {
-
/**
* Ask the homeserver to change the password.
* @param password Current password.
* @param newPassword New password
*/
fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable
+
+ /**
+ * Deactivate the account.
+ *
+ * This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register
+ * the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account
+ * details from your identity server. This action is irreversible.\n\nDeactivating your account does not by default
+ * cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.
+ *
+ * Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not
+ * be shared with any new or unregistered users, but registered users who already have access to these messages will still
+ * have access to their copy.
+ *
+ * @param password the account password
+ * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
+ * an incomplete view of conversations
+ */
+ fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
index ab8417b542..e6fbaaf9a6 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt
@@ -111,6 +111,8 @@ interface CryptoService {
roomId: String,
callback: MatrixCallback)
+ fun discardOutboundSession(roomId: String)
+
@Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
index 9a3107a8ca..3cdd433516 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt
@@ -81,6 +81,9 @@ object EventType {
// Relation Events
const val REACTION = "m.reaction"
+ // Unwedging
+ internal const val DUMMY = "m.dummy"
+
private val STATE_EVENTS = listOf(
STATE_ROOM_NAME,
STATE_ROOM_TOPIC,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
index eb4a9b59e4..d7d6682046 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
@@ -104,6 +104,7 @@ interface Timeline {
interface Listener {
/**
* Call when the timeline has been updated through pagination or sync.
+ * The latest event is the first in the list
* @param snapshot the most up to date snapshot
*/
fun onTimelineUpdated(snapshot: List)
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..a789814958 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,13 +21,13 @@ 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
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
-import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
@@ -45,7 +45,9 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
+import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
+import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
@@ -59,6 +61,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
+import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@@ -72,13 +75,16 @@ import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
+import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.whereType
+import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.MoshiProvider
+import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
@@ -116,12 +122,15 @@ import kotlin.math.max
internal class DefaultCryptoService @Inject constructor(
// Olm Manager
private val olmManager: OlmManager,
- // The credentials,
- private val credentials: Credentials,
+ @UserId
+ private val userId: String,
+ @DeviceId
+ private val deviceId: String?,
private val myDeviceInfoHolder: Lazy,
// the crypto store
private val cryptoStore: IMXCryptoStore,
-
+ // Room encryptors store
+ private val roomEncryptorsStore: RoomEncryptorsStore,
// Olm device
private val olmDevice: MXOlmDevice,
// Set of parameters used to configure/customize the end-to-end crypto.
@@ -162,7 +171,10 @@ internal class DefaultCryptoService @Inject constructor(
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
- private val cryptoCoroutineScope: CoroutineScope
+ private val cryptoCoroutineScope: CoroutineScope,
+ private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
+ private val sendToDeviceTask: SendToDeviceTask,
+ private val messageEncrypter: MessageEncrypter
) : CryptoService {
init {
@@ -171,11 +183,13 @@ internal class DefaultCryptoService @Inject constructor(
private val uiHandler = Handler(Looper.getMainLooper())
- // MXEncrypting instance for each room.
- private val roomEncryptors: MutableMap = HashMap()
private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
+ // The date of the last time we forced establishment
+ // of a new session for each user:device.
+ private val lastNewSessionForcedDates = MXUsersDevicesMap()
+
fun onStateEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
@@ -199,7 +213,7 @@ internal class DefaultCryptoService @Inject constructor(
this.callback = object : MatrixCallback {
override fun onSuccess(data: Unit) {
// bg refresh of crypto device
- downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback())
+ downloadKeys(listOf(userId), true, NoOpMatrixCallback())
callback.onSuccess(data)
}
@@ -398,7 +412,7 @@ internal class DefaultCryptoService @Inject constructor(
}
/**
- * Provides the device information for a device id and a user Id
+ * Provides the device information for a user id and a device Id
*
* @param userId the user id
* @param deviceId the device id
@@ -493,14 +507,14 @@ internal class DefaultCryptoService @Inject constructor(
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
- Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
+ Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false
}
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
if (!encryptingClass) {
- Timber.e("## setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
+ Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
return false
}
@@ -511,9 +525,7 @@ internal class DefaultCryptoService @Inject constructor(
else -> olmEncryptionFactory.create(roomId)
}
- synchronized(roomEncryptors) {
- roomEncryptors.put(roomId, alg)
- }
+ roomEncryptorsStore.put(roomId, alg)
// if encryption was not previously enabled in this room, we will have been
// ignoring new device events for these users so far. We may well have
@@ -591,42 +603,44 @@ internal class DefaultCryptoService @Inject constructor(
callback: MatrixCallback) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
- Timber.v("## encryptEventContent() : wait after e2e init")
+ Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
internalStart(false)
}
val userIds = getRoomUserIds(roomId)
- var alg = synchronized(roomEncryptors) {
- roomEncryptors[roomId]
- }
+ var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
val algorithm = getEncryptionAlgorithm(roomId)
if (algorithm != null) {
if (setEncryptionInRoom(roomId, algorithm, false, userIds)) {
- synchronized(roomEncryptors) {
- alg = roomEncryptors[roomId]
- }
+ alg = roomEncryptorsStore.get(roomId)
}
}
}
val safeAlgorithm = alg
if (safeAlgorithm != null) {
val t0 = System.currentTimeMillis()
- Timber.v("## encryptEventContent() starts")
+ Timber.v("## CRYPTO | encryptEventContent() starts")
runCatching {
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
- Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
+ Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}.foldToCallback(callback)
} else {
val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
- Timber.e("## encryptEventContent() : $reason")
+ Timber.e("## CRYPTO | encryptEventContent() : $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
}
}
}
+ override fun discardOutboundSession(roomId: String) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ roomEncryptorsStore.get(roomId)?.discardSessionKey()
+ }
+ }
+
/**
* Decrypt an event
*
@@ -664,20 +678,42 @@ internal class DefaultCryptoService @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error
*/
+ @Throws(MXCryptoError::class)
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content
if (eventContent == null) {
- Timber.e("## decryptEvent : empty event content")
+ Timber.e("## CRYPTO | decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
- Timber.e("## decryptEvent() : $reason")
+ Timber.e("## CRYPTO | decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else {
- return alg.decryptEvent(event, timeline)
+ try {
+ return alg.decryptEvent(event, timeline)
+ } catch (mxCryptoError: MXCryptoError) {
+ Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+ if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
+ if (mxCryptoError is MXCryptoError.Base
+ && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
+ // need to find sending device
+ val olmContent = event.content.toModel()
+ cryptoStore.getUserDevices(event.senderId ?: "")
+ ?.values
+ ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
+ ?.let {
+ markOlmSessionForUnwedging(event.senderId ?: "", it)
+ }
+ ?: run {
+ Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
+ }
+ }
+ }
+ throw mxCryptoError
+ }
}
}
}
@@ -730,30 +766,30 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel() ?: return
- Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
+ Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
- Timber.e("## GOSSIP onRoomKeyEvent() : missing fields")
+ Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
return
}
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
if (alg == null) {
- Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
+ Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return
}
alg.onRoomKeyEvent(event, keysBackupService)
}
private fun onSecretSendReceived(event: Event) {
- Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
+ Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
if (!event.isEncrypted()) {
// secret send messages must be encrypted
- Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event")
+ Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
- if (event.senderId != credentials.userId) {
- Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
+ if (event.senderId != userId) {
+ Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
return
}
@@ -763,13 +799,13 @@ internal class DefaultCryptoService @Inject constructor(
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
if (existingRequest == null) {
- Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
+ Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
- Timber.v("## onSecretSend() : secret not handled by SDK")
+ Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK")
}
}
@@ -805,7 +841,7 @@ internal class DefaultCryptoService @Inject constructor(
try {
loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
- Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
+ Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
@@ -835,16 +871,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the membership event causing the change
*/
private fun onRoomMembershipEvent(roomId: String, event: Event) {
- val alg: IMXEncrypting?
+ roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
- synchronized(roomEncryptors) {
- alg = roomEncryptors[roomId]
- }
-
- if (null == alg) {
- // No encrypting in this room
- return
- }
event.stateKey?.let { userId ->
val roomMember: RoomMemberSummary? = event.content.toModel()
val membership = roomMember?.membership
@@ -938,13 +966,13 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
- Timber.v("## importRoomKeys starts")
+ Timber.v("## CRYPTO | importRoomKeys starts")
val t0 = System.currentTimeMillis()
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
val t1 = System.currentTimeMillis()
- Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
+ Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
val importedSessions = MoshiProvider.providesMoshi()
.adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
@@ -952,7 +980,7 @@ internal class DefaultCryptoService @Inject constructor(
val t2 = System.currentTimeMillis()
- Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms")
+ Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
if (importedSessions == null) {
throw Exception("Error")
@@ -1087,7 +1115,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel() ?: return Unit.also {
- Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
+ Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content")
}
val requestBody = RoomKeyRequestBody(
@@ -1102,18 +1130,18 @@ internal class DefaultCryptoService @Inject constructor(
override fun requestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel() ?: return Unit.also {
- Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
+ Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
- Timber.v("## requestRoomKeyForEvent() : wait after e2e init")
+ Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
internalStart(false)
}
roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event) ?: run {
- Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
+ Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
}
}
}
@@ -1136,6 +1164,39 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
}
+ private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
+ val deviceKey = deviceInfo.identityKey()
+
+ val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+ val now = System.currentTimeMillis()
+ if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+ Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+ return
+ }
+
+ Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+ lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+
+ // Now send a blank message on that session so the other side knows about it.
+ // (The keyshare request is sent in the clear so that won't do)
+ // We send this first such that, as long as the toDevice messages arrive in the
+ // same order we sent them, the other end will get this first, set up the new session,
+ // then get the keyshare request and send the key over this new session (because it
+ // is the session it has most recently received a message on).
+ val payloadJson = mapOf("type" to EventType.DUMMY)
+
+ val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+ val sendToDeviceMap = MXUsersDevicesMap()
+ sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
+ Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+ val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+ sendToDeviceTask.execute(sendToDeviceParams)
+ }
+ }
+
/**
* Provides the list of unknown devices
*
@@ -1178,7 +1239,7 @@ internal class DefaultCryptoService @Inject constructor(
* ========================================================================================== */
override fun toString(): String {
- return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
+ return "DefaultCryptoService of $userId ($deviceId)"
}
override fun getOutgoingRoomKeyRequest(): List {
@@ -1192,4 +1253,15 @@ internal class DefaultCryptoService @Inject constructor(
override fun getGossipingEventsTrail(): List {
return cryptoStore.getGossipingEventsTrail()
}
+
+ /* ==========================================================================================
+ * For test only
+ * ========================================================================================== */
+
+ @VisibleForTesting
+ val cryptoStoreForTesting = cryptoStore
+
+ companion object {
+ const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
index 37a5ee18e1..680539d057 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt
@@ -108,7 +108,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':'))
}
} catch (e: Exception) {
- Timber.e(e, "## canRetryKeysDownload() failed")
+ Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
}
}
@@ -137,7 +137,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
- Timber.v("## startTrackingDeviceList() : Now tracking device list for $userId")
+ Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
@@ -161,7 +161,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
- Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
+ Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
@@ -169,7 +169,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) {
- Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
+ Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true
}
@@ -259,7 +259,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param forceDownload Always download the keys even if cached.
*/
suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap {
- Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
+ Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
// Map from userId -> deviceId -> MXDeviceInfo
val stored = MXUsersDevicesMap()
@@ -288,13 +288,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
}
return if (downloadUsers.isEmpty()) {
- Timber.v("## downloadKeys() : no new user device")
+ Timber.v("## CRYPTO | downloadKeys() : no new user device")
stored
} else {
- Timber.v("## downloadKeys() : starts")
+ Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = System.currentTimeMillis()
val result = doKeyDownloadForUsers(downloadUsers)
- Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
+ Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
result.also {
it.addEntriesFromMap(stored)
}
@@ -307,7 +307,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param downloadUsers the user ids list
*/
private suspend fun doKeyDownloadForUsers(downloadUsers: List): MXUsersDevicesMap {
- Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
+ Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
// get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) {
@@ -318,16 +318,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val response = try {
downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) {
- Timber.e(throwable, "##doKeyDownloadForUsers(): error")
+ Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
onKeysDownloadFailed(filteredUsers)
throw throwable
}
- Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
+ Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
for (userId in filteredUsers) {
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
- Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models")
+ Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap()
for ((deviceId, deviceInfo) in models) {
@@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// Handle cross signing keys update
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
- Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
+ Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
}
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
- Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
+ Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
}
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
- Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
+ Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
}
cryptoStore.storeUserCrossSigningKeys(
userId,
@@ -395,28 +395,28 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
if (null == deviceKeys) {
- Timber.e("## validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
return false
}
if (null == deviceKeys.keys) {
- Timber.e("## validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
return false
}
if (null == deviceKeys.signatures) {
- Timber.e("## validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
return false
}
// Check that the user_id and device_id in the received deviceKeys are correct
if (deviceKeys.userId != userId) {
- Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
return false
}
if (deviceKeys.deviceId != deviceId) {
- Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
return false
}
@@ -424,21 +424,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val signKey = deviceKeys.keys[signKeyId]
if (null == signKey) {
- Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
+ Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
return false
}
val signatureMap = deviceKeys.signatures[userId]
if (null == signatureMap) {
- Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
+ Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
return false
}
val signature = signatureMap[signKeyId]
if (null == signature) {
- Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
+ Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
return false
}
@@ -453,7 +453,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
if (!isVerified) {
- Timber.e("## validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
+ Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
+ deviceKeys.deviceId + " with error " + errorMessage)
return false
}
@@ -464,12 +464,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
- Timber.e("## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
+ Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
+ deviceKeys.deviceId + " has changed : "
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
- Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
- Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
+ Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
+ Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
return false
}
@@ -501,10 +501,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
doKeyDownloadForUsers(users)
}.fold(
{
- Timber.v("## refreshOutdatedDeviceLists() : done")
+ Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
},
{
- Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
+ Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
}
)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt
index da596960dd..38f81ba47d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingGossipingRequestManager.kt
@@ -32,7 +32,10 @@ import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObje
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.session.SessionScope
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -43,7 +46,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val cryptoConfig: MXCryptoConfig,
private val gossipingWorkManager: GossipingWorkManager,
- private val roomDecryptorProvider: RoomDecryptorProvider) {
+ private val roomEncryptorsStore: RoomEncryptorsStore,
+ private val roomDecryptorProvider: RoomDecryptorProvider,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope) {
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync.
@@ -90,7 +96,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
* @param event the announcement event.
*/
fun onGossipingRequestEvent(event: Event) {
- Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
+ Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel()
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) {
@@ -155,7 +161,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
receivedRequestCancellations?.forEach { request ->
- Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
@@ -178,17 +184,42 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
- val userId = request.userId
- val deviceId = request.deviceId
- val body = request.requestBody
- val roomId = body!!.roomId
- val alg = body.algorithm
+ val userId = request.userId ?: return
+ val deviceId = request.deviceId ?: return
+ val body = request.requestBody ?: return
+ val roomId = body.roomId ?: return
+ val alg = body.algorithm ?: return
- Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
- if (userId == null || credentials.userId != userId) {
- // TODO: determine if we sent this device the keys already: in
- Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
+ Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
+ if (credentials.userId != userId) {
+ Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
+ val senderKey = body.senderKey ?: return Unit
+ .also { Timber.w("missing senderKey") }
+ .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
+ val sessionId = body.sessionId ?: return Unit
+ .also { Timber.w("missing sessionId") }
+ .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
+
+ if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
+ return Unit
+ .also { Timber.w("Only megolm is accepted here") }
+ .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
+ }
+
+ val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
+ .also { Timber.w("no room Encryptor") }
+ .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
+
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
+
+ if (isSuccess) {
+ cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
+ } else {
+ cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
+ }
+ }
+ cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
return
}
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@@ -196,18 +227,18 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) {
- Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
+ Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (!decryptor.hasKeysForKeyRequest(request)) {
- Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
+ Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (credentials.deviceId == deviceId && credentials.userId == userId) {
- Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@@ -219,16 +250,16 @@ internal class IncomingGossipingRequestManager @Inject constructor(
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
// if the device is verified already, share the keys
- val device = cryptoStore.getUserDevice(userId, deviceId!!)
+ val device = cryptoStore.getUserDevice(userId, deviceId)
if (device != null) {
if (device.isVerified) {
- Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
request.share?.run()
return
}
if (device.isBlocked) {
- Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@@ -236,7 +267,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// As per config we automatically discard untrusted devices request
if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
- Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
+ Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
// At this point the device is unknown, we don't want to bother user with that
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
@@ -249,30 +280,30 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name")
+ Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name")
}
val userId = request.userId
if (userId == null || credentials.userId != userId) {
- Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
+ Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val deviceId = request.deviceId
?: return Unit.also {
- Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
+ Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
val device = cryptoStore.getUserDevice(userId, deviceId)
?: return Unit.also {
- Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
+ Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
if (!device.isVerified || device.isBlocked) {
- Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
+ Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
@@ -289,7 +320,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
else -> null
}?.let { secretValue ->
- Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
+ Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
val params = SendGossipWorker.Params(
sessionId = sessionId,
@@ -301,13 +332,13 @@ internal class IncomingGossipingRequestManager @Inject constructor(
val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
} else {
- Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
+ Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
return
}
- Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
+ Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
request.ignore = Runnable {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
@@ -341,7 +372,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
try {
listener.onRoomKeyRequest(request)
} catch (e: Exception) {
- Timber.e(e, "## onRoomKeyRequest() failed")
+ Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed")
}
}
}
@@ -358,7 +389,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
return
}
} catch (e: Exception) {
- Timber.e(e, "## GOSSIP onRoomKeyRequest() failed")
+ Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed")
}
}
}
@@ -377,7 +408,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
try {
listener.onRoomKeyRequestCancellation(request)
} catch (e: Exception) {
- Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed")
+ Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed")
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
index 86f0768a7d..d6d8b06b5f 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt
@@ -342,6 +342,8 @@ internal class MXOlmDevice @Inject constructor(
} catch (e: Exception) {
Timber.e(e, "## encryptMessage() : failed")
}
+ } else {
+ Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
}
return res
@@ -625,6 +627,7 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown.
*/
+ @Throws(MXCryptoError::class)
fun decryptGroupMessage(body: String,
roomId: String,
timeline: String?,
@@ -662,8 +665,7 @@ internal class MXOlmDevice @Inject constructor(
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.e("## decryptGroupMessage() : fails to parse the payload")
- throw
- MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
return OlmDecryptionResult(
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
index c06f10b106..eb1c07cb92 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
@@ -55,7 +55,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
- Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
+ Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
@@ -72,7 +72,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
- Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it")
+ Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
return@launch
}
@@ -113,7 +113,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
?: // no request was made for this key
return Unit.also {
- Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request")
+ Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody")
}
sendOutgoingRoomKeyRequestCancellation(req, andResend)
@@ -125,7 +125,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request
*/
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
- Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
+ Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,
@@ -143,7 +143,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request
*/
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
- Timber.v("$request")
+ Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request")
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomEncryptorsStore.kt
new file mode 100644
index 0000000000..52a324d68d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomEncryptorsStore.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
+import im.vector.matrix.android.internal.session.SessionScope
+import javax.inject.Inject
+
+@SessionScope
+internal class RoomEncryptorsStore @Inject constructor() {
+
+ // MXEncrypting instance for each room.
+ private val roomEncryptors = mutableMapOf()
+
+ fun put(roomId: String, alg: IMXEncrypting) {
+ synchronized(roomEncryptors) {
+ roomEncryptors.put(roomId, alg)
+ }
+ }
+
+ fun get(roomId: String): IMXEncrypting? {
+ return synchronized(roomEncryptors) {
+ roomEncryptors[roomId]
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
index e1cac0d75f..e630d14eab 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
@@ -25,10 +25,11 @@ import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDe
import timber.log.Timber
import javax.inject.Inject
-internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice,
- private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
+internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
+ private val olmDevice: MXOlmDevice,
+ private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
- suspend fun handle(devicesByUser: Map>): MXUsersDevicesMap {
+ suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap {
val devicesWithoutSession = ArrayList()
val results = MXUsersDevicesMap()
@@ -40,7 +41,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val sessionId = olmDevice.getSessionId(key!!)
- if (sessionId.isNullOrEmpty()) {
+ if (sessionId.isNullOrEmpty() || force) {
devicesWithoutSession.add(deviceInfo)
}
@@ -68,11 +69,11 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
//
// That should eventually resolve itself, but it's poor form.
- Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
+ Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
- Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
+ Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
for ((userId, deviceInfos) in devicesByUser) {
for (deviceInfo in deviceInfos) {
var oneTimeKey: MXKey? = null
@@ -80,7 +81,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
if (null != deviceIds) {
for (deviceId in deviceIds) {
val olmSessionResult = results.getObject(userId, deviceId)
- if (olmSessionResult!!.sessionId != null) {
+ if (olmSessionResult!!.sessionId != null && !force) {
// We already have a result for this device
continue
}
@@ -89,7 +90,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
oneTimeKey = key
}
if (oneTimeKey == null) {
- Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
+ Timber.v("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
+ " for device " + userId + " : " + deviceId)
continue
}
@@ -125,14 +126,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
if (!sessionId.isNullOrEmpty()) {
- Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
+ Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
} else {
// Possibly a bad key
- Timber.e("## verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
+ Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
}
} else {
- Timber.e("## verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId
+ Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId
+ ":" + deviceId + " Error " + errorMessage)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt
index fae205e581..c1cdbe59f9 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt
@@ -16,19 +16,24 @@
package im.vector.matrix.android.internal.crypto.actions
-import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
+import im.vector.matrix.android.internal.di.DeviceId
+import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber
import javax.inject.Inject
-internal class MessageEncrypter @Inject constructor(private val credentials: Credentials,
- private val olmDevice: MXOlmDevice) {
-
+internal class MessageEncrypter @Inject constructor(
+ @UserId
+ private val userId: String,
+ @DeviceId
+ private val deviceId: String?,
+ private val olmDevice: MXOlmDevice) {
/**
* Encrypt an event payload for a list of devices.
* This method must be called from the getCryptoHandler() thread.
@@ -37,13 +42,13 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
* @param deviceInfos list of device infos to encrypt for.
* @return the content for an m.room.encrypted event.
*/
- fun encryptMessage(payloadFields: Map, deviceInfos: List): EncryptedMessage {
+ fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage {
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
val payloadJson = payloadFields.toMutableMap()
- payloadJson["sender"] = credentials.userId
- payloadJson["sender_device"] = credentials.deviceId!!
+ payloadJson["sender"] = userId
+ payloadJson["sender_device"] = deviceId!!
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
@@ -53,11 +58,9 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
- val keysMap = HashMap()
- keysMap["ed25519"] = olmDevice.deviceEd25519Key!!
- payloadJson["keys"] = keysMap
+ payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!)
- val ciphertext = HashMap()
+ val ciphertext = mutableMapOf()
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
val sessionId = olmDevice.getSessionId(deviceKey)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt
index e9176ad6d9..0babb73842 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt
@@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.crypto.algorithms
+import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
@@ -35,6 +36,7 @@ internal interface IMXDecrypting {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error
*/
+ @Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/**
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt
index 555ce9dfd4..ddf605def4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt
@@ -33,4 +33,34 @@ internal interface IMXEncrypting {
* @return the encrypted content
*/
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content
+
+ /**
+ * In Megolm, each recipient maintains a record of the ratchet value which allows
+ * them to decrypt any messages sent in the session after the corresponding point
+ * in the conversation. If this value is compromised, an attacker can similarly
+ * decrypt past messages which were encrypted by a key derived from the
+ * compromised or subsequent ratchet values. This gives 'partial' forward
+ * secrecy.
+ *
+ * To mitigate this issue, the application should offer the user the option to
+ * discard historical conversations, by winding forward any stored ratchet values,
+ * or discarding sessions altogether.
+ */
+ fun discardSessionKey()
+
+ /**
+ * Re-shares a session key with devices if the key has already been
+ * sent to them.
+ *
+ * @param sessionId The id of the outbound session to share.
+ * @param userId The id of the user who owns the target device.
+ * @param deviceId The id of the target device.
+ * @param senderKey The key of the originating device for the session.
+ *
+ * @return true in case of success
+ */
+ suspend fun reshareKey(sessionId: String,
+ userId: String,
+ deviceId: String,
+ senderKey: String): Boolean
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 1d7a2765fa..59ffa5f874 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -63,6 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
*/
private var pendingEvents: MutableMap>> = HashMap()
+ @Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
@@ -70,7 +71,9 @@ internal class MXMegolmDecryption(private val userId: String,
return decryptEvent(event, timeline, requestOnFail)
}
+ @Throws(MXCryptoError::class)
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
+ Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
@@ -188,7 +191,7 @@ internal class MXMegolmDecryption(private val userId: String,
val events = timeline.getOrPut(timelineId) { ArrayList() }
if (event !in events) {
- Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
+ Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
events.add(event)
}
}
@@ -199,6 +202,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param event the key event.
*/
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
+ Timber.v("## CRYPTO | onRoomKeyEvent()")
var exportFormat = false
val roomKeyContent = event.getClearContent().toModel() ?: return
@@ -207,11 +211,11 @@ internal class MXMegolmDecryption(private val userId: String,
val forwardingCurve25519KeyChain: MutableList = ArrayList()
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
- Timber.e("## onRoomKeyEvent() : Key event is missing fields")
+ Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields")
return
}
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
- Timber.v("## onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" +
+ Timber.v("## CRYPTO | onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" +
" sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}")
val forwardedRoomKeyContent = event.getClearContent().toModel()
?: return
@@ -221,7 +225,7 @@ internal class MXMegolmDecryption(private val userId: String,
}
if (senderKey == null) {
- Timber.e("## onRoomKeyEvent() : event is missing sender_key field")
+ Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field")
return
}
@@ -230,18 +234,18 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat = true
senderKey = forwardedRoomKeyContent.senderKey
if (null == senderKey) {
- Timber.e("## onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
+ Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
return
}
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
- Timber.e("## forwarded_room_key_event is missing sender_claimed_ed25519_key field")
+ Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field")
return
}
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
} else {
- Timber.v("## onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
+ Timber.v("## CRYPTO | onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
if (null == senderKey) {
@@ -253,6 +257,7 @@ internal class MXMegolmDecryption(private val userId: String,
keysClaimed = event.getKeysClaimed().toMutableMap()
}
+ Timber.e("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey,
roomKeyContent.roomId,
@@ -284,7 +289,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param sessionId the session id
*/
override fun onNewSession(senderKey: String, sessionId: String) {
- Timber.v("ON NEW SESSION $sessionId - $senderKey")
+ Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(null, senderKey, sessionId)
}
@@ -318,7 +323,7 @@ internal class MXMegolmDecryption(private val userId: String,
// were no one-time keys.
return@mapCatching
}
- Timber.v("## shareKeysWithDevice() : sharing keys for session" +
+ Timber.v("## CRYPTO | shareKeysWithDevice() : sharing keys for session" +
" ${body.senderKey}|${body.sessionId} with device $userId:$deviceId")
val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY)
@@ -337,7 +342,7 @@ internal class MXMegolmDecryption(private val userId: String,
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
- Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
+ Timber.v("## CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index a2d21c4f89..3800e3c4f2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -40,7 +40,7 @@ import timber.log.Timber
internal class MXMegolmEncryption(
// The id of the room we will be sending to.
- private var roomId: String,
+ private val roomId: String,
private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore,
@@ -66,17 +66,25 @@ internal class MXMegolmEncryption(
override suspend fun encryptEventContent(eventContent: Content,
eventType: String,
userIds: List): Content {
+ val ts = System.currentTimeMillis()
+ Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds)
+ Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
val outboundSession = ensureOutboundSession(devices)
return encryptContent(outboundSession, eventType, eventContent)
}
+ override fun discardSessionKey() {
+ outboundSession = null
+ }
+
/**
* Prepare a new session.
*
* @return the session description
*/
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
+ Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSession()
val keysClaimedMap = HashMap()
@@ -96,6 +104,7 @@ internal class MXMegolmEncryption(
* @param devicesInRoom the devices list
*/
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo {
+ Timber.v("## CRYPTO | ensureOutboundSession start")
var session = outboundSession
if (session == null
// Need to make a brand new session?
@@ -132,7 +141,7 @@ internal class MXMegolmEncryption(
devicesByUsers: Map>) {
// nothing to send, the task is done
if (devicesByUsers.isEmpty()) {
- Timber.v("## shareKey() : nothing more to do")
+ Timber.v("## CRYPTO | shareKey() : nothing more to do")
return
}
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
@@ -145,7 +154,7 @@ internal class MXMegolmEncryption(
break
}
}
- Timber.v("## shareKey() ; userId ${subMap.keys}")
+ Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices)
@@ -174,10 +183,10 @@ internal class MXMegolmEncryption(
payload["content"] = submap
var t0 = System.currentTimeMillis()
- Timber.v("## shareUserDevicesKey() : starts")
+ Timber.v("## CRYPTO | shareUserDevicesKey() : starts")
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
- Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
+ Timber.v("## CRYPTO | shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
+ (System.currentTimeMillis() - t0) + " ms")
val contentMap = MXUsersDevicesMap()
var haveTargets = false
@@ -200,17 +209,17 @@ internal class MXMegolmEncryption(
// so just skip it.
continue
}
- Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
+ Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
haveTargets = true
}
}
if (haveTargets) {
t0 = System.currentTimeMillis()
- Timber.v("## shareUserDevicesKey() : has target")
+ Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
sendToDeviceTask.execute(sendToDeviceParams)
- Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after "
+ Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
+ (System.currentTimeMillis() - t0) + " ms")
// Add the devices we have shared with to session.sharedWithDevices.
@@ -224,7 +233,7 @@ internal class MXMegolmEncryption(
}
}
} else {
- Timber.v("## shareUserDevicesKey() : no need to sharekey")
+ Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
}
}
@@ -305,4 +314,49 @@ internal class MXMegolmEncryption(
throw MXCryptoError.UnknownDevice(unknownDevices)
}
}
+
+ override suspend fun reshareKey(sessionId: String,
+ userId: String,
+ deviceId: String,
+ senderKey: String): Boolean {
+ Timber.d("[MXMegolmEncryption] reshareKey: $sessionId to $userId:$deviceId")
+ val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
+ .also { Timber.w("Device not found") }
+
+ // Get the chain index of the key we previously sent this device
+ val chainIndex = outboundSession?.sharedWithDevices?.getObject(userId, deviceId)?.toLong() ?: return false
+ .also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") }
+
+ val devicesByUser = mapOf(userId to listOf(deviceInfo))
+ val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
+ val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
+ olmSessionResult?.sessionId
+ ?: // no session with this device, probably because there were no one-time keys.
+ // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it.
+ return false
+
+ Timber.d("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
+
+ val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY)
+
+ runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
+ .fold(
+ {
+ // TODO
+ payloadJson["content"] = it.exportKeys(chainIndex) ?: ""
+ },
+ {
+ // TODO
+ }
+
+ )
+
+ val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+ val sendToDeviceMap = MXUsersDevicesMap()
+ sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
+ Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
+ val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+ sendToDeviceTask.execute(sendToDeviceParams)
+ return true
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt
index 0a8ef3993b..8ef527fa05 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt
@@ -38,6 +38,7 @@ internal class MXOlmDecryption(
private val userId: String)
: IMXDecrypting {
+ @Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val olmEventContent = event.content.toModel() ?: run {
Timber.e("## decryptEvent() : bad event format")
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
index 899e884e0d..a9b84a8e48 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt
@@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
internal class MXOlmEncryption(
- private var roomId: String,
+ private val roomId: String,
private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val messageEncrypter: MessageEncrypter,
@@ -78,4 +78,13 @@ internal class MXOlmEncryption(
deviceListManager.downloadKeys(users, false)
ensureOlmSessionsForUsersAction.handle(users)
}
+
+ override fun discardSessionKey() {
+ // No need for olm
+ }
+
+ override suspend fun reshareKey(sessionId: String, userId: String, deviceId: String, senderKey: String): Boolean {
+ // No need for olm
+ return false
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
index cf1a3b237a..9be08d9f2d 100755
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
@@ -103,10 +103,11 @@ class OlmInboundGroupSessionWrapper : Serializable {
/**
* Export the inbound group session keys
+ * @param index the index to export. If null, the first known index will be used
*
* @return the inbound group session as MegolmSessionData if the operation succeeds
*/
- fun exportKeys(): MegolmSessionData? {
+ fun exportKeys(index: Long? = null): MegolmSessionData? {
return try {
if (null == forwardingCurve25519KeyChain) {
forwardingCurve25519KeyChain = ArrayList()
@@ -116,6 +117,8 @@ class OlmInboundGroupSessionWrapper : Serializable {
return null
}
+ val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex
+
MegolmSessionData(
senderClaimedEd25519Key = keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!),
@@ -123,7 +126,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
senderClaimedKeys = keysClaimed,
roomId = roomId,
sessionId = olmInboundGroupSession!!.sessionIdentifier(),
- sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex),
+ sessionKey = olmInboundGroupSession!!.export(wantedIndex),
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
)
} catch (e: Exception) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DummyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DummyContent.kt
new file mode 100644
index 0000000000..b52354768d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DummyContent.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.model.rest
+
+/**
+ * Class representing the dummy content
+ * Ref: https://matrix.org/docs/spec/client_server/latest#id82
+ */
+typealias DummyContent = Unit
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt
index cf8652352c..ea5fb26d83 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt
@@ -20,28 +20,53 @@ import com.squareup.moshi.JsonClass
/**
* Class representing the forward room key request body content
+ * Ref: https://matrix.org/docs/spec/client_server/latest#m-forwarded-room-key
*/
@JsonClass(generateAdapter = true)
data class ForwardedRoomKeyContent(
-
+ /**
+ * Required. The encryption algorithm the key in this event is to be used with.
+ */
@Json(name = "algorithm")
val algorithm: String? = null,
+ /**
+ * Required. The room where the key is used.
+ */
@Json(name = "room_id")
val roomId: String? = null,
+ /**
+ * Required. The Curve25519 key of the device which initiated the session originally.
+ */
@Json(name = "sender_key")
val senderKey: String? = null,
+ /**
+ * Required. The ID of the session that the key is for.
+ */
@Json(name = "session_id")
val sessionId: String? = null,
+ /**
+ * Required. The key to be exchanged.
+ */
@Json(name = "session_key")
val sessionKey: String? = null,
+ /**
+ * Required. Chain of Curve25519 keys. It starts out empty, but each time the key is forwarded to another device,
+ * the previous sender in the chain is added to the end of the list. For example, if the key is forwarded
+ * from A to B to C, this field is empty between A and B, and contains A's Curve25519 key between B and C.
+ */
@Json(name = "forwarding_curve25519_key_chain")
val forwardingCurve25519KeyChain: List? = null,
+ /**
+ * Required. The Ed25519 key of the device which initiated the session originally. It is 'claimed' because the
+ * receiving device has no way to tell that the original room_key actually came from a device which owns the
+ * private part of this key unless they have done device verification.
+ */
@Json(name = "sender_claimed_ed25519_key")
val senderClaimedEd25519Key: String? = null
)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
index 649a5a118f..7db3d6ead3 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
@@ -419,7 +419,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2
- || keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
+ && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
// Unsupported algorithm
return IntegrityResult.Error(
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt
index a8f65e9219..0d1026b69f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt
@@ -196,7 +196,8 @@ internal interface IMXCryptoStore {
*/
fun storeUserDevices(userId: String, devices: Map?)
- fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?,
+ fun storeUserCrossSigningKeys(userId: String,
+ masterKey: CryptoCrossSigningKey?,
selfSigningKey: CryptoCrossSigningKey?,
userSigningKey: CryptoCrossSigningKey?)
@@ -262,7 +263,7 @@ internal interface IMXCryptoStore {
* @param deviceKey the public key of the other device.
* @return The Base64 end-to-end session, or null if not found
*/
- fun getDeviceSession(sessionId: String?, deviceKey: String?): OlmSessionWrapper?
+ fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper?
/**
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt
index bd51cf8539..a6f3f5d593 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt
@@ -555,11 +555,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
- override fun getDeviceSession(sessionId: String?, deviceKey: String?): OlmSessionWrapper? {
- if (sessionId == null || deviceKey == null) {
- return null
- }
-
+ override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
// If not in cache (or not found), try to read it from realm
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt
index 0f3da0c834..15c91a629a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt
@@ -51,7 +51,7 @@ internal class UserAgentHolder @Inject constructor(private val context: Context,
appName = pm.getApplicationLabel(appInfo).toString()
val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0)
- appVersion = pkgInfo.versionName
+ appVersion = pkgInfo.versionName ?: ""
// Use appPackageName instead of appName if appName contains any non-ASCII character
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt
index 23d8210e89..62aded3f03 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt
@@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.account
-import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@@ -30,4 +29,12 @@ internal interface AccountAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
fun changePassword(@Body params: ChangePasswordParams): Call
+
+ /**
+ * Deactivate the user account
+ *
+ * @param params the deactivate account params
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate")
+ fun deactivate(@Body params: DeactivateAccountParams): Call
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt
index 87e003b0d3..032139ce5d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt
@@ -39,6 +39,9 @@ internal abstract class AccountModule {
@Binds
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
+ @Binds
+ abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask
+
@Binds
abstract fun bindAccountService(service: DefaultAccountService): AccountService
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt
rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt
index 83ce8eb0aa..8aa1f9834c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.matrix.android.api.session.account.model
+package im.vector.matrix.android.internal.session.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt
index ecd4b309d8..6400253d9f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt
@@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt
new file mode 100644
index 0000000000..9960f61dbc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.session.account
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
+
+@JsonClass(generateAdapter = true)
+internal data class DeactivateAccountParams(
+ @Json(name = "auth")
+ val auth: UserPasswordAuth? = null,
+
+ // Set to true to erase all data of the account
+ @Json(name = "erase")
+ val erase: Boolean
+) {
+ companion object {
+ fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams {
+ return DeactivateAccountParams(
+ auth = UserPasswordAuth(user = userId, password = password),
+ erase = erase
+ )
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt
new file mode 100644
index 0000000000..f5b105cfee
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.session.account
+
+import im.vector.matrix.android.internal.di.UserId
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.session.cleanup.CleanupSession
+import im.vector.matrix.android.internal.task.Task
+import org.greenrobot.eventbus.EventBus
+import javax.inject.Inject
+
+internal interface DeactivateAccountTask : Task {
+ data class Params(
+ val password: String,
+ val eraseAllData: Boolean
+ )
+}
+
+internal class DefaultDeactivateAccountTask @Inject constructor(
+ private val accountAPI: AccountAPI,
+ private val eventBus: EventBus,
+ @UserId private val userId: String,
+ private val cleanupSession: CleanupSession
+) : DeactivateAccountTask {
+
+ override suspend fun execute(params: DeactivateAccountTask.Params) {
+ val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData)
+
+ executeRequest(eventBus) {
+ apiCall = accountAPI.deactivate(deactivateAccountParams)
+ }
+
+ cleanupSession.handle()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt
index fce01994d3..f6db1dd3db 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt
@@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
+ private val deactivateAccountTask: DeactivateAccountTask,
private val taskExecutor: TaskExecutor) : AccountService {
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable {
@@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
}
.executeBy(taskExecutor)
}
+
+ override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable {
+ return deactivateAccountTask
+ .configureWith(DeactivateAccountTask.Params(password, eraseAllData)) {
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt
new file mode 100644
index 0000000000..ebd0fad39c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.session.cleanup
+
+import im.vector.matrix.android.BuildConfig
+import im.vector.matrix.android.internal.SessionManager
+import im.vector.matrix.android.internal.auth.SessionParamsStore
+import im.vector.matrix.android.internal.crypto.CryptoModule
+import im.vector.matrix.android.internal.database.RealmKeysUtils
+import im.vector.matrix.android.internal.di.CryptoDatabase
+import im.vector.matrix.android.internal.di.SessionCacheDirectory
+import im.vector.matrix.android.internal.di.SessionDatabase
+import im.vector.matrix.android.internal.di.SessionFilesDirectory
+import im.vector.matrix.android.internal.di.SessionId
+import im.vector.matrix.android.internal.di.UserMd5
+import im.vector.matrix.android.internal.di.WorkManagerProvider
+import im.vector.matrix.android.internal.session.SessionModule
+import im.vector.matrix.android.internal.session.cache.ClearCacheTask
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import timber.log.Timber
+import java.io.File
+import javax.inject.Inject
+
+internal class CleanupSession @Inject constructor(
+ private val workManagerProvider: WorkManagerProvider,
+ @SessionId private val sessionId: String,
+ private val sessionManager: SessionManager,
+ private val sessionParamsStore: SessionParamsStore,
+ @SessionDatabase private val clearSessionDataTask: ClearCacheTask,
+ @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
+ @SessionFilesDirectory private val sessionFiles: File,
+ @SessionCacheDirectory private val sessionCache: File,
+ private val realmKeysUtils: RealmKeysUtils,
+ @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
+ @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
+ @UserMd5 private val userMd5: String
+) {
+ suspend fun handle() {
+ Timber.d("Cleanup: release session...")
+ sessionManager.releaseSession(sessionId)
+
+ Timber.d("Cleanup: cancel pending works...")
+ workManagerProvider.cancelAllWorks()
+
+ Timber.d("Cleanup: delete session params...")
+ sessionParamsStore.delete(sessionId)
+
+ Timber.d("Cleanup: clear session data...")
+ clearSessionDataTask.execute(Unit)
+
+ Timber.d("Cleanup: clear crypto data...")
+ clearCryptoDataTask.execute(Unit)
+
+ Timber.d("Cleanup: clear file system")
+ sessionFiles.deleteRecursively()
+ sessionCache.deleteRecursively()
+
+ Timber.d("Cleanup: clear the database keys")
+ realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
+ realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
+
+ // Sanity check
+ if (BuildConfig.DEBUG) {
+ Realm.getGlobalInstanceCount(realmSessionConfiguration)
+ .takeIf { it > 0 }
+ ?.let { Timber.e("All realm instance for session has not been closed ($it)") }
+ Realm.getGlobalInstanceCount(realmCryptoConfiguration)
+ .takeIf { it > 0 }
+ ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
index b14a7758c5..610ade5744 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
@@ -16,58 +16,31 @@
package im.vector.matrix.android.internal.session.signout
-import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
-import im.vector.matrix.android.internal.SessionManager
-import im.vector.matrix.android.internal.auth.SessionParamsStore
-import im.vector.matrix.android.internal.crypto.CryptoModule
-import im.vector.matrix.android.internal.database.RealmKeysUtils
-import im.vector.matrix.android.internal.di.CryptoDatabase
-import im.vector.matrix.android.internal.di.SessionCacheDirectory
-import im.vector.matrix.android.internal.di.SessionDatabase
-import im.vector.matrix.android.internal.di.SessionFilesDirectory
-import im.vector.matrix.android.internal.di.SessionId
-import im.vector.matrix.android.internal.di.UserMd5
-import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.network.executeRequest
-import im.vector.matrix.android.internal.session.SessionModule
-import im.vector.matrix.android.internal.session.cache.ClearCacheTask
+import im.vector.matrix.android.internal.session.cleanup.CleanupSession
import im.vector.matrix.android.internal.task.Task
-import io.realm.Realm
-import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
-import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject
internal interface SignOutTask : Task {
data class Params(
- val sigOutFromHomeserver: Boolean
+ val signOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(
- private val workManagerProvider: WorkManagerProvider,
- @SessionId private val sessionId: String,
private val signOutAPI: SignOutAPI,
- private val sessionManager: SessionManager,
- private val sessionParamsStore: SessionParamsStore,
- @SessionDatabase private val clearSessionDataTask: ClearCacheTask,
- @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
- @SessionFilesDirectory private val sessionFiles: File,
- @SessionCacheDirectory private val sessionCache: File,
- private val realmKeysUtils: RealmKeysUtils,
- @SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
- @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
- @UserMd5 private val userMd5: String,
- private val eventBus: EventBus
+ private val eventBus: EventBus,
+ private val cleanupSession: CleanupSession
) : SignOutTask {
override suspend fun execute(params: SignOutTask.Params) {
// It should be done even after a soft logout, to be sure the deviceId is deleted on the
- if (params.sigOutFromHomeserver) {
+ if (params.signOutFromHomeserver) {
Timber.d("SignOut: send request...")
try {
executeRequest(eventBus) {
@@ -87,37 +60,7 @@ internal class DefaultSignOutTask @Inject constructor(
}
}
- Timber.d("SignOut: release session...")
- sessionManager.releaseSession(sessionId)
-
- Timber.d("SignOut: cancel pending works...")
- workManagerProvider.cancelAllWorks()
-
- Timber.d("SignOut: delete session params...")
- sessionParamsStore.delete(sessionId)
-
- Timber.d("SignOut: clear session data...")
- clearSessionDataTask.execute(Unit)
-
- Timber.d("SignOut: clear crypto data...")
- clearCryptoDataTask.execute(Unit)
-
- Timber.d("SignOut: clear file system")
- sessionFiles.deleteRecursively()
- sessionCache.deleteRecursively()
-
- Timber.d("SignOut: clear the database keys")
- realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
- realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
-
- // Sanity check
- if (BuildConfig.DEBUG) {
- Realm.getGlobalInstanceCount(realmSessionConfiguration)
- .takeIf { it > 0 }
- ?.let { Timber.e("All realm instance for session has not been closed ($it)") }
- Realm.getGlobalInstanceCount(realmCryptoConfiguration)
- .takeIf { it > 0 }
- ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
- }
+ Timber.d("SignOut: cleanup session...")
+ cleanupSession.handle()
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt
index 10e7ceb692..86ca561dfe 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt
@@ -39,10 +39,10 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
toDevice.events?.forEachIndexed { index, event ->
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
// Decrypt event if necessary
- decryptEvent(event, null)
+ decryptToDeviceEvent(event, null)
if (event.getClearType() == EventType.MESSAGE
&& event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") {
- Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
+ Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
} else {
verificationService.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event)
@@ -61,28 +61,24 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
* @param timelineId the timeline identifier
* @return true if the event has been decrypted
*/
- private fun decryptEvent(event: Event, timelineId: String?): Boolean {
+ private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
+ Timber.v("## CRYPTO | decryptToDeviceEvent")
if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null
try {
result = cryptoService.decryptEvent(event, timelineId ?: "")
} catch (exception: MXCryptoError) {
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError)
+ Timber.e("## CRYPTO | Failed to decrypt to device event: ${event.mCryptoError ?: exception}")
}
if (null != result) {
-// event.mxDecryptionResult = MXDecryptionResult(
-// payload = result.clearEvent,
-// keysClaimed = map
-// )
- // TODO persist that?
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
-// event.setClearData(result)
return true
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index c2f2959bd7..d22d80c4b3 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
+import im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.devtools.AccountDataFragment
@@ -445,8 +446,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(BootstrapMigrateBackupFragment::class)
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(DeactivateAccountFragment::class)
+ fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
}
diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
index bc5a1aff95..a5ec0591f5 100644
--- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
@@ -49,6 +49,7 @@ data class MainActivityArgs(
val clearCache: Boolean = false,
val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false,
+ val isAccountDeactivated: Boolean = false,
val isSoftLogout: Boolean = false
) : Parcelable
@@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() {
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
+ isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
@@ -121,7 +123,14 @@ class MainActivity : VectorBaseActivity() {
return
}
when {
- args.clearCredentials -> session.signOut(
+ args.isAccountDeactivated -> {
+ // Just do the local cleanup
+ Timber.w("Account deactivated, start app")
+ sessionHolder.clearActiveSession()
+ doLocalCleanup()
+ startNextActivityAndFinish()
+ }
+ args.clearCredentials -> session.signOut(
!args.isUserLoggedOut,
object : MatrixCallback {
override fun onSuccess(data: Unit) {
@@ -135,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
displayError(failure)
}
})
- args.clearCache -> session.clearCache(
+ args.clearCache -> session.clearCache(
object : MatrixCallback {
override fun onSuccess(data: Unit) {
doLocalCleanup()
@@ -182,16 +191,16 @@ class MainActivity : VectorBaseActivity() {
private fun startNextActivityAndFinish() {
val intent = when {
args.clearCredentials
- && !args.isUserLoggedOut ->
- // User has explicitly asked to log out
+ && (!args.isUserLoggedOut || args.isAccountDeactivated) ->
+ // User has explicitly asked to log out or deactivated his account
LoginActivity.newIntent(this, null)
- args.isSoftLogout ->
+ args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this)
- args.isUserLoggedOut ->
+ args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
SignedOutActivity.newIntent(this)
- sessionHolder.hasActiveSession() ->
+ sessionHolder.hasActiveSession() ->
// We have a session.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
@@ -200,7 +209,7 @@ class MainActivity : VectorBaseActivity() {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
}
- else ->
+ else ->
// First start, or no active session
LoginActivity.newIntent(this, null)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
index 72f686c2c8..d98ebcfa73 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
@@ -44,6 +44,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
SHRUG("/shrug", "", R.string.command_description_shrug),
PLAIN("/plain", "", R.string.command_description_plain),
+ DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session),
// TODO temporary command
VERIFY_USER("/verify", "", R.string.command_description_verify);
diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
index 875fe92610..e7d2e9a62b 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
@@ -281,6 +281,9 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.POLL)
}
}
+ Command.DISCARD_SESSION.command -> {
+ ParsedCommand.DiscardSession
+ }
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
index e4fee27ee6..63e016b0b6 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
@@ -52,4 +52,5 @@ sealed class ParsedCommand {
class SendShrug(val message: CharSequence) : ParsedCommand()
class VerifyUser(val userId: String) : ParsedCommand()
class SendPoll(val question: String, val options: List) : ParsedCommand()
+ object DiscardSession: ParsedCommand()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
index 12674e5cd2..3ae206cd21 100644
--- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
@@ -28,6 +28,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
+import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@@ -37,6 +38,7 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData
import kotlinx.android.synthetic.main.activity.*
+import java.net.HttpURLConnection
import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() {
@@ -91,8 +93,14 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
if (error is CreateRoomFailure.CreatedWithTimeout) {
finish()
} else {
+ val message = if (error is Failure.ServerError && error.httpCode == HttpURLConnection.HTTP_INTERNAL_ERROR /*500*/) {
+ // This error happen if the invited userId does not exist.
+ getString(R.string.create_room_dm_failure)
+ } else {
+ errorFormatter.toHumanReadable(error)
+ }
AlertDialog.Builder(this)
- .setMessage(errorFormatter.toHumanReadable(error))
+ .setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt
index 016806f319..1c38e6f723 100644
--- a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt
@@ -23,6 +23,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
+import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
@@ -56,15 +57,29 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
override fun buildModels() {
val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank()
- val asyncUsers = currentState.directoryUsers
- when (asyncUsers) {
+ when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading()
- is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch)
+ is Success -> renderSuccess(
+ computeUsersList(asyncUsers(), currentState.directorySearchTerm),
+ currentState.selectedUsers.map { it.userId },
+ hasSearch
+ )
is Fail -> renderFailure(asyncUsers.error)
}
}
+ /**
+ * Eventually add the searched terms, if it is a userId, and if not already present in the result
+ */
+ private fun computeUsersList(directoryUsers: List, searchTerms: String): List {
+ return directoryUsers +
+ searchTerms
+ .takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
+ ?.let { listOf(User(it)) }
+ .orEmpty()
+ }
+
private fun renderLoading() {
loadingItem {
id("loading")
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
index f5f92c381d..6a2b7825ac 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
@@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
+import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
@@ -73,7 +74,8 @@ data class VerificationBottomSheetViewState(
val isMe: Boolean = false,
val currentDeviceCanCrossSign: Boolean = false,
val userWantsToCancel: Boolean = false,
- val userThinkItsNotHim: Boolean = false
+ val userThinkItsNotHim: Boolean = false,
+ val quadSContainsSecrets: Boolean = true
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(
@@ -116,6 +118,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
+ val ssssOk = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(
+ listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
+ null // default key
+ ) is IntegrityResult.Success
setState {
copy(
otherUserMxItem = userItem?.toMatrixItem(),
@@ -126,7 +132,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
selfVerificationMode = selfVerificationMode,
roomId = args.roomId,
isMe = args.otherUserId == session.myUserId,
- currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign()
+ currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
+ quadSContainsSecrets = ssssOk
)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
index 56c76bc2b0..a1b55832d5 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
@@ -65,14 +65,16 @@ class VerificationRequestController @Inject constructor(
title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
}
- bottomSheetVerificationActionItem {
- id("passphrase")
- title(stringProvider.getString(R.string.verification_cannot_access_other_session))
- titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
- subTitle(stringProvider.getString(R.string.verification_use_passphrase))
- iconRes(R.drawable.ic_arrow_right)
- iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
- listener { listener?.onClickRecoverFromPassphrase() }
+ if (state.quadSContainsSecrets) {
+ bottomSheetVerificationActionItem {
+ id("passphrase")
+ title(stringProvider.getString(R.string.verification_cannot_access_other_session))
+ titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
+ subTitle(stringProvider.getString(R.string.verification_use_passphrase))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
+ listener { listener?.onClickRecoverFromPassphrase() }
+ }
}
} else {
val styledText =
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
index 2449c635e4..ac2e2b7fa9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
@@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
@@ -92,6 +93,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.subscribe { sharedAction ->
when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
+ is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
@@ -99,7 +101,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
BootstrapBottomSheet.show(supportFragmentManager, true)
}
- }
+ }.exhaustive
}
.disposeOnDestroy()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt
index 902ea93588..a074c0e879 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt
@@ -23,6 +23,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
*/
sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
+ object CloseDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction()
object PromptForSecurityBootstrap : HomeActivitySharedAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
index 9aa9313ad2..a8373797c6 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
@@ -33,10 +33,15 @@ class HomeDrawerFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {
+ private lateinit var sharedActionViewModel: HomeSharedActionViewModel
+
override fun getLayoutResId() = R.layout.fragment_home_drawer
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+
+ sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
+
if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
}
@@ -49,11 +54,13 @@ class HomeDrawerFragment @Inject constructor(
}
}
homeDrawerHeaderSettingsView.setOnClickListener {
+ sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openSettings(requireActivity())
}
// Debug menu
homeDrawerHeaderDebugView.setOnClickListener {
+ sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity())
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index cef172da73..d0dcac6ecc 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -447,6 +447,19 @@ class RoomDetailViewModel @AssistedInject constructor(
// TODO
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
+ is ParsedCommand.DiscardSession -> {
+ if (room.isEncrypted()) {
+ session.cryptoService().discardOutboundSession(room.roomId)
+ _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
+ popDraft()
+ } else {
+ _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
+ _viewEvents.post(
+ RoomDetailViewEvents
+ .ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
+ )
+ }
+ }
}.exhaustive
}
is SendMode.EDIT -> {
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
index 57f914118b..8b89aeda2a 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt
@@ -19,6 +19,7 @@ package im.vector.riotx.features.login
import android.os.Build
import android.os.Bundle
import android.view.View
+import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import butterknife.OnClick
@@ -40,7 +41,8 @@ import kotlinx.android.synthetic.main.fragment_login.*
import javax.inject.Inject
/**
- * In this screen, in signin mode:
+ * In this screen:
+ * In signin mode:
* - the user is asked for login (or email) and password to sign in to a homeserver.
* - He also can reset his password
* In signup mode:
@@ -49,6 +51,7 @@ import javax.inject.Inject
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false
+ private var isSignupMode = false
override fun getLayoutResId() = R.layout.fragment_login
@@ -57,6 +60,14 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
setupSubmitButton()
setupPasswordReveal()
+
+ passwordField.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ submit()
+ return@setOnEditorActionListener true
+ }
+ return@setOnEditorActionListener false
+ }
}
private fun setupAutoFill(state: LoginViewState) {
@@ -82,7 +93,20 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
val login = loginField.text.toString()
val password = passwordField.text.toString()
- loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx)))
+ // This can be called by the IME action, so deal with empty cases
+ var error = 0
+ if (login.isEmpty()) {
+ loginFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_user_name else R.string.error_empty_field_enter_user_name)
+ error++
+ }
+ if (password.isEmpty()) {
+ passwordFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_password else R.string.error_empty_field_your_password)
+ error++
+ }
+
+ if (error == 0) {
+ loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx)))
+ }
}
private fun cleanupUi() {
@@ -190,6 +214,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
}
override fun updateWithState(state: LoginViewState) {
+ isSignupMode = state.signMode == SignMode.SignUp
+
setupUi(state)
setupAutoFill(state)
setupButtons(state)
diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
index 2984cc3889..ae8e7f23fc 100644
--- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
@@ -191,7 +191,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.postDelayed(
{
- refreshNotificationDrawerBg()
+ try {
+ refreshNotificationDrawerBg()
+ } catch (throwable: Throwable) {
+ // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
+ Timber.w(throwable, "refreshNotificationDrawerBg failure")
+ }
}, 200)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
index f0a5a8ace8..e765f961dd 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
@@ -159,7 +159,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY"
private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK"
private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY"
- const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY"
private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"
private const val MEDIA_SAVING_3_DAYS = 0
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
index 5db14fdbd2..6d00f02c97 100755
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt
@@ -20,6 +20,7 @@ import android.content.Intent
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
private var keyToHighlight: String? = null
+ var ignoreInvalidTokenError = false
+
@Inject lateinit var session: Session
override fun injectWith(injector: ScreenComponent) {
@@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
- EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
+ EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
@@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
return keyToHighlight
}
+ override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
+ if (ignoreInvalidTokenError) {
+ Timber.w("Ignoring invalid token global error")
+ } else {
+ super.handleInvalidToken(globalError)
+ }
+ }
+
companion object {
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
index f754064fbc..802cf7b33f 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
@@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
false
}
-
- // Deactivate account section
-
- // deactivate account
- findPreference(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!!
- .onPreferenceClickListener = Preference.OnPreferenceClickListener {
- activity?.let {
- notImplemented()
- // TODO startActivity(DeactivateAccountActivity.getIntent(it))
- }
-
- false
- }
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt
new file mode 100644
index 0000000000..f5130d5e00
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.riotx.features.settings.account.deactivation
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.extensions.showPassword
+import im.vector.riotx.core.platform.VectorBaseActivity
+import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.features.MainActivity
+import im.vector.riotx.features.MainActivityArgs
+import im.vector.riotx.features.settings.VectorSettingsActivity
+import kotlinx.android.synthetic.main.fragment_deactivate_account.*
+import javax.inject.Inject
+
+class DeactivateAccountFragment @Inject constructor(
+ val viewModelFactory: DeactivateAccountViewModel.Factory
+) : VectorBaseFragment() {
+
+ private val viewModel: DeactivateAccountViewModel by fragmentViewModel()
+
+ override fun getLayoutResId() = R.layout.fragment_deactivate_account
+
+ override fun onResume() {
+ super.onResume()
+ (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
+ }
+
+ private var settingsActivity: VectorSettingsActivity? = null
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ settingsActivity = context as? VectorSettingsActivity
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ settingsActivity = null
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupUi()
+ setupViewListeners()
+ observeViewEvents()
+ }
+
+ private fun setupUi() {
+ deactivateAccountPassword.textChanges()
+ .subscribe {
+ deactivateAccountPasswordTil.error = null
+ deactivateAccountSubmit.isEnabled = it.isNotEmpty()
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun setupViewListeners() {
+ deactivateAccountPasswordReveal.setOnClickListener {
+ viewModel.handle(DeactivateAccountAction.TogglePassword)
+ }
+
+ deactivateAccountSubmit.setOnClickListener {
+ viewModel.handle(DeactivateAccountAction.DeactivateAccount(
+ deactivateAccountPassword.text.toString(),
+ deactivateAccountEraseCheckbox.isChecked))
+ }
+ }
+
+ private fun observeViewEvents() {
+ viewModel.observeViewEvents {
+ when (it) {
+ is DeactivateAccountViewEvents.Loading -> {
+ settingsActivity?.ignoreInvalidTokenError = true
+ showLoadingDialog(it.message)
+ }
+ DeactivateAccountViewEvents.EmptyPassword -> {
+ settingsActivity?.ignoreInvalidTokenError = false
+ deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
+ }
+ DeactivateAccountViewEvents.InvalidPassword -> {
+ settingsActivity?.ignoreInvalidTokenError = false
+ deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
+ }
+ is DeactivateAccountViewEvents.OtherFailure -> {
+ settingsActivity?.ignoreInvalidTokenError = false
+ displayErrorDialog(it.throwable)
+ }
+ DeactivateAccountViewEvents.Done ->
+ MainActivity.restartApp(activity!!, MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
+ }.exhaustive
+ }
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ deactivateAccountPassword.showPassword(state.passwordShown)
+ deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt
new file mode 100644
index 0000000000..4e7f7252e2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.riotx.features.settings.account.deactivation
+
+import im.vector.riotx.core.platform.VectorViewEvents
+
+/**
+ * Transient events for deactivate account settings screen
+ */
+sealed class DeactivateAccountViewEvents : VectorViewEvents {
+ data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
+ object EmptyPassword : DeactivateAccountViewEvents()
+ object InvalidPassword : DeactivateAccountViewEvents()
+ data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
+ object Done : DeactivateAccountViewEvents()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt
new file mode 100644
index 0000000000..adfc9ff5ae
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.riotx.features.settings.account.deactivation
+
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.failure.isInvalidPassword
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.core.platform.VectorViewModelAction
+
+data class DeactivateAccountViewState(
+ val passwordShown: Boolean = false
+) : MvRxState
+
+sealed class DeactivateAccountAction : VectorViewModelAction {
+ object TogglePassword : DeactivateAccountAction()
+ data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction()
+}
+
+class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
+ private val session: Session)
+ : VectorViewModel(initialState) {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
+ }
+
+ override fun handle(action: DeactivateAccountAction) {
+ when (action) {
+ DeactivateAccountAction.TogglePassword -> handleTogglePassword()
+ is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
+ }.exhaustive
+ }
+
+ private fun handleTogglePassword() = withState {
+ setState {
+ copy(passwordShown = !passwordShown)
+ }
+ }
+
+ private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
+ if (action.password.isEmpty()) {
+ _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
+ return
+ }
+
+ _viewEvents.post(DeactivateAccountViewEvents.Loading())
+
+ session.deactivateAccount(action.password, action.eraseAllData, object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ _viewEvents.post(DeactivateAccountViewEvents.Done)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ if (failure.isInvalidPassword()) {
+ _viewEvents.post(DeactivateAccountViewEvents.InvalidPassword)
+ } else {
+ _viewEvents.post(DeactivateAccountViewEvents.OtherFailure(failure))
+ }
+ }
+ })
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ @JvmStatic
+ override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? {
+ val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment()
+ return fragment.viewModelFactory.create(state)
+ }
+ }
+}
diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml
new file mode 100644
index 0000000000..1bf04ba81e
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_deactivate_account.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml
index 3f2443440e..894a7598c8 100644
--- a/vector/src/main/res/layout/fragment_login.xml
+++ b/vector/src/main/res/layout/fragment_login.xml
@@ -82,6 +82,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
+ android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
@@ -104,19 +105,18 @@
-
+ android:layout_marginTop="22dp">
+ android:text="@string/auth_forgot_password"
+ app:layout_constraintStart_toStartOf="parent" />
-
+
diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
index 7a63e14bf6..7a851025c9 100644
--- a/vector/src/main/res/values/strings_riotX.xml
+++ b/vector/src/main/res/values/strings_riotX.xml
@@ -6,12 +6,14 @@
-
+ Forces the current outbound group session in an encrypted room to be discarded
+ Only supported in encrypted rooms
-
+ Please choose a username.
+ Please choose a password.
@@ -30,4 +32,5 @@
+ "We couldn't create your DM. Please check the users you want to invite and try again."
diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml
index ac8fb8445c..11f16655a7 100644
--- a/vector/src/main/res/xml/vector_settings_general.xml
+++ b/vector/src/main/res/xml/vector_settings_general.xml
@@ -97,14 +97,13 @@
-
+
+ android:persistent="false"
+ android:title="@string/settings_deactivate_my_account"
+ app:fragment="im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment" />