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 @@ + + + + + + + + + + + + + + + + + + + + + + + + +