diff --git a/changelog.d/5853.feature b/changelog.d/5853.feature new file mode 100644 index 00000000000..2a399e76aad --- /dev/null +++ b/changelog.d/5853.feature @@ -0,0 +1 @@ +Improve user experience when he is first invited to a room. Users will be able to decrypt and view previous messages diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 7dafe33935f..a78953caacc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -18,12 +18,15 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri +import android.util.Log import androidx.lifecycle.Observer import androidx.test.internal.runner.junit4.statement.UiThreadStatement import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -38,7 +41,10 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -47,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.sync.SyncState import timber.log.Timber import java.util.UUID +import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit * This class exposes methods to be used in common cases * Registration, login, Sync, Sending messages... */ -class CommonTestHelper private constructor(context: Context) { +class CommonTestHelper internal constructor(context: Context) { companion object { internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { @@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) { return sentEvents } + fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { + waitWithLatch { latch -> + retryPeriodicallyWithLatch(latch) { + val roomSummary = otherSession.getRoomSummary(roomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("# TEST", "${otherSession.myUserId} can see the invite") + } + } + } + } + + // not sure why it's taking so long :/ + runBlockingTest(90_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") + try { + otherSession.roomService().joinRoom(roomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + waitWithLatch { + retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.getRoomSummary(roomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } + /** * Reply in a thread * @param room the room where to send the messages @@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) { ) assertNotNull(session) return session.also { + // most of the test was created pre-MSC3061 so ensure compatibility + it.cryptoService().enableShareKeyOnInvite(false) trackedSessions.add(session) } } @@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) { * @param latch * @throws InterruptedException */ - fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { + fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) { assertTrue( "Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.", - latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS).also { + if (!it) { + // cancel job on timeout + job?.cancel("Await timeout") + } + } ) } suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { while (true) { - delay(1000) + try { + delay(1000) + } catch (ex: CancellationException) { + // the job was canceled, just stop + return + } if (condition()) { latch.countDown() return @@ -447,10 +497,10 @@ class CommonTestHelper private constructor(context: Context) { fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { val latch = CountDownLatch(1) - coroutineScope.launch(dispatcher) { + val job = coroutineScope.launch(dispatcher) { block(latch) } - await(latch, timeout) + await(latch, timeout, job) } fun runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 5fd86d4fdb8..f36bfb6210e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { + fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) + aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = roomHistoryVisibility + name = "MyRoom" + }) } if (encryptedRoom) { testHelper.waitWithLatch { latch -> @@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt new file mode 100644 index 00000000000..32d63a19342 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -0,0 +1,298 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import org.amshove.kluent.internal.assertEquals +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CryptoTestData +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.TestMatrixCallback + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2EShareKeysConfigTest : InstrumentedTest { + + @Test + fun msc3061ShouldBeDisabledByDefault() = runCryptoTest(context()) { _, commonTestHelper -> + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + Assert.assertFalse("MSC3061 is lab and should be disabled by default", aliceSession.cryptoService().isShareKeysOnInviteEnabled()) + } + + @Test + fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + aliceSession.cryptoService().enableShareKeyOnInvite(false) + val roomId = commonTestHelper.runBlockingTest { + aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true + } + } + val roomAlice = aliceSession.roomService().getRoom(roomId)!! + + // send some messages + val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + aliceSession.cryptoService().discardOutboundSession(roomId) + val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + + // Create bob account + val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) + + // Let alice invite bob + commonTestHelper.runBlockingTest { + roomAlice.membershipService().invite(bobSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) + + // Bob has join but should not be able to decrypt history + cryptoTestHelper.ensureCannotDecrypt( + withSession1.map { it.eventId } + withSession2.map { it.eventId }, + bobSession, + roomId + ) + + // We don't need bob anymore + commonTestHelper.signOutAndClose(bobSession) + + // Now let's enable history key sharing on alice side + aliceSession.cryptoService().enableShareKeyOnInvite(true) + + // let's add a new message first + val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1) + + // Worth nothing to check that the session was rotated + Assert.assertNotEquals( + "Session should have been rotated", + withSession2.first().root.content?.get("session_id")!!, + afterFlagOn.first().root.content?.get("session_id")!! + ) + + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + commonTestHelper.runBlockingTest { + roomAlice.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session + cryptoTestHelper.ensureCannotDecrypt( + withSession1.map { it.eventId } + withSession2.map { it.eventId }, + samSession, + roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + afterFlagOn.map { it.eventId }, + samSession, + roomId, + afterFlagOn.map { it.root.getClearContent()?.get("body") as String }) + } + + @Test + fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + val aliceSession = testData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(false) + } + val bobSession = testData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + + val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession) + + // Bob should have shared history keys to sam. + // But has alice hasn't enabled sharing, bob shouldn't send her sessions + cryptoTestHelper.ensureCannotDecrypt( + fromAliceNotSharable.map { it.eventId }, + samSession, + testData.roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + fromBobSharable.map { it.eventId }, + samSession, + testData.roomId, + fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) + } + + @Test + fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + val aliceSession = testData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val bobSession = testData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + + val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession) + + cryptoTestHelper.ensureCanDecrypt( + fromAliceNotSharable.map { it.eventId }, + samSession, + testData.roomId, + fromAliceNotSharable.map { it.root.getClearContent()?.get("body") as String }) + + cryptoTestHelper.ensureCanDecrypt( + fromBobSharable.map { it.eventId }, + samSession, + testData.roomId, + fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) + } + + private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple, List, Session> { + val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1) + val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1) + + // Now let bob invite Sam + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let bob invite sam + commonTestHelper.runBlockingTest { + bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId) + return Triple(fromAliceNotSharable, fromBobSharable, samSession) + } + + // test flag on backup is correct + + @Test + fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + aliceSession.cryptoService().enableShareKeyOnInvite(false) + val roomId = commonTestHelper.runBlockingTest { + aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true + } + } + val roomAlice = aliceSession.roomService().getRoom(roomId)!! + + // send some messages + val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + aliceSession.cryptoService().enableShareKeyOnInvite(true) + val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + + Log.v("#E2E TEST", "Create and start key backup for bob ...") + val keysBackupService = aliceSession.cryptoService().keysBackupService() + val keyBackupPassword = "FooBarBaz" + val megolmBackupCreationInfo = commonTestHelper.doSync { + keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) + } + val version = commonTestHelper.doSync { + keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) + } + + commonTestHelper.waitWithLatch { latch -> + keysBackupService.backupAllGroupSessions( + null, + TestMatrixCallback(latch, true) + ) + } + + // signout + commonTestHelper.signOutAndClose(aliceSession) + + val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + newAliceSession.cryptoService().enableShareKeyOnInvite(true) + + newAliceSession.cryptoService().keysBackupService().let { kbs -> + val keyVersionResult = commonTestHelper.doSync { + kbs.getVersion(version.version, it) + } + + val importedResult = commonTestHelper.doSync { + kbs.restoreKeyBackupWithPassword( + keyVersionResult!!, + keyBackupPassword, + null, + null, + null, + it + ) + } + + assertEquals(2, importedResult.totalNumberOfKeys) + } + + // Now let's invite sam + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + commonTestHelper.runBlockingTest { + newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) + } + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session + cryptoTestHelper.ensureCannotDecrypt( + notSharableMessage.map { it.eventId }, + samSession, + roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + sharableMessage.map { it.eventId }, + samSession, + roomId, + sharableMessage.map { it.root.getClearContent()?.get("body") as String }) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index 5a61eee7fe3..251c13ccbf4 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -23,7 +23,6 @@ import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -49,9 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.mustFail import java.util.concurrent.CountDownLatch +// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest -@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") class E2eeSanityTests : InstrumentedTest { @get:Rule val rule = RetryTestRule(3) @@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest { // All user should accept invite otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) + testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") } @@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) + testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID) } ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) @@ -740,37 +737,6 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } - } - } - } - - // not sure why it's taking so long :/ - testHelper.runBlockingTest(90_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.roomService().joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } - } - - Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } - } - } - private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> sentEventIds.forEach { sentEventId -> diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt new file mode 100644 index 00000000000..32a95008b1a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -0,0 +1,424 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.internal.assertNotEquals +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.shouldShareHistory +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeShareKeysHistoryTest : InstrumentedTest { + + @Test + fun testShareMessagesHistoryWithRoomWorldReadable() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE) + } + + @Test + fun testShareMessagesHistoryWithRoomShared() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED) + } + + @Test + fun testShareMessagesHistoryWithRoomJoined() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED) + } + + @Test + fun testShareMessagesHistoryWithRoomInvited() { + testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED) + } + + /** + * In this test we create a room and test that new members + * can decrypt history when the room visibility is + * RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE. + * We should not be able to view messages/decrypt otherwise + */ + private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = + runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) + + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! + + // Bob + val bobSession = cryptoTestData.secondSession!!.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!! + + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") + + val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) + Assert.assertTrue("Message should be sent", aliceMessageId != null) + Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") + + // Bob should be able to decrypt the message + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") + } + } + } + } + + // Create a new user + val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also { + it.cryptoService().enableShareKeyOnInvite(true) + } + Log.v("#E2E TEST", "Aris user created") + + // Alice invites new user to the room + testHelper.runBlockingTest { + Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") + aliceRoomPOV.membershipService().invite(arisSession.myUserId) + } + + waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) + + ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper) + Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID") + + when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE, + RoomHistoryVisibility.SHARED, + null + -> { + // Aris should be able to decrypt the message + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE + ).also { + if (it) { + Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") + } + } + } + } + } + RoomHistoryVisibility.INVITED, + RoomHistoryVisibility.JOINED -> { + // Aris should not even be able to get the message + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + timelineEvent == null + } + } + } + } + + testHelper.signOutAndClose(arisSession) + cryptoTestData.cleanUp(testHelper) + } + + @Test + fun testNeedsRotationFromWorldReadableToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromWorldReadableToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromWorldReadableToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromSharedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable")) + } + + @Test + fun testNeedsRotationFromSharedToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromSharedToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromInvitedToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromInvitedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable")) + } + + @Test + fun testNeedsRotationFromInvitedToJoined() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined")) + } + + @Test + fun testNeedsRotationFromJoinedToShared() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared")) + } + + @Test + fun testNeedsRotationFromJoinedToInvited() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited")) + } + + @Test + fun testNeedsRotationFromJoinedToWorldReadable() { + testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable")) + } + + /** + * In this test we will test that a rotation is needed when + * When the room's history visibility setting changes to world_readable or shared + * from invited or joined, or changes to invited or joined from world_readable or shared, + * senders that support this flag must rotate their megolm sessions. + */ + private fun testRotationDueToVisibilityChange( + initRoomHistoryVisibility: RoomHistoryVisibility, + nextRoomHistoryVisibility: RoomHistoryVisibilityContent + ) { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) + val e2eRoomID = cryptoTestData.roomId + + // Alice + val aliceSession = cryptoTestData.firstSession.also { + it.cryptoService().enableShareKeyOnInvite(true) + } + val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! +// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + + // Bob + val bobSession = cryptoTestData.secondSession!! + + val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!! + + assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) + Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID") + + val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) + Assert.assertTrue("Message should be sent", aliceMessageId != null) + Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID") + + // Bob should be able to decrypt the message + var firstAliceMessageMegolmSessionId: String? = null + val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) + } + } + } + } + + Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) + + var secondAliceMessageSessionId: String? = null + sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(secondMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) + } + } + } + } + } + assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId) + Log.v("#E2E TEST ROTATION", "No rotation needed yet") + + // Let's change the room history visibility + testHelper.runBlockingTest { + aliceRoomPOV.stateService() + .sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + body = RoomHistoryVisibilityContent( + historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr + ).toContent() + ) + } + + // ensure that the state did synced down + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content + ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility + } + } + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val roomVisibility = aliceSession.getRoom(e2eRoomID)!! + .stateService() + .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") + roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility + } + } + + var aliceThirdMessageSessionId: String? = null + sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(thirdMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String + } + } + } + } + } + + when { + initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> { + assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId) + Log.v("#E2E TEST ROTATION", "Rotation is not needed") + } + initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> { + assertNotEquals("Session should have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId) + Log.v("#E2E TEST ROTATION", "Rotation is needed!") + } + } + + cryptoTestData.cleanUp(testHelper) + } + + private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { + return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId + } + + private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + } + + private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") + } + } + } + } + + testHelper.runBlockingTest(60_000) { + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.roomService().joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + } + + Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index e37ae5be86f..e8e7b1d7084 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -72,7 +72,7 @@ class PreShareKeysTest : InstrumentedTest { assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) - val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier() + val megolmSessionId = bobInboundForAlice.session.sessionIdentifier() assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt index 45fdb9e1e30..cf201611a0c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -19,14 +19,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestData -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper /** * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] */ internal data class KeysBackupScenarioData( val cryptoTestData: CryptoTestData, - val aliceKeys: List, + val aliceKeys: List, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index fb498e0de5d..e160938721f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest { val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo // - Check encryptGroupSession() returns stg - val keyBackupData = keysBackup.encryptGroupSession(session) + val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } assertNotNull(keyBackupData) assertNotNull(keyBackupData!!.sessionData) @@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest { val sessionData = keysBackup .decryptKeyBackupData( keyBackupData, - session.olmInboundGroupSession!!.sessionIdentifier(), + session.safeSessionId!!, cryptoTestData.roomId, decryption!! ) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 38f94c51039..2cc2b506b92 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -187,7 +187,7 @@ internal class KeysBackupTestHelper( // - 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!!) + .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!) Assert.assertNotNull(aliceKey2) assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 38136ff5cee..2cd579df24d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest { val roomName = "My Space" val topic = "A public space for test" var spaceId: String = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceId = session.spaceService().createSpace(roomName, topic, null, true) - // wait a bit to let the summary update it self :/ - it.countDown() } - Thread.sleep(4_000) - val syncedSpace = session.spaceService().getSpace(spaceId) commonTestHelper.waitWithLatch { commonTestHelper.retryPeriodicallyWithLatch(it) { - syncedSpace?.asRoom()?.roomSummary()?.name != null + session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null } } + + val syncedSpace = session.spaceService().getSpace(spaceId) assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name) assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic) // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 63ca963479f..80020665f87 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -20,7 +20,6 @@ import android.util.Log import androidx.lifecycle.Observer import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Ignore @@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest { val spaceName = "My Space" val topic = "A public space for test" var spaceId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceId = session.spaceService().createSpace(spaceName, topic, null, true) - it.countDown() } val syncedSpace = session.spaceService().getSpace(spaceId) var roomId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) - it.countDown() } val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { syncedSpace!!.addChildren(roomId, viaServers, null, true) - it.countDown() } - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - it.countDown() } - Thread.sleep(9000) - - val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents - val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } - - parents?.forEach { - Log.d("## TEST", "parent : $it") + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + parents?.forEach { + Log.d("## TEST", "parent : $it") + } + parents?.size == 1 && + parents.first().roomSummary?.name == spaceName && + canonicalParents?.size == 1 && + canonicalParents.first().roomSummary?.name == spaceName + } } - - assertNotNull(parents) - assertEquals(1, parents!!.size) - assertEquals(spaceName, parents.first().roomSummary?.name) - - assertNotNull(canonicalParents) - assertEquals(1, canonicalParents!!.size) - assertEquals(spaceName, canonicalParents.first().roomSummary?.name) } // @Test @@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest { // } @Test - fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) /* val spaceBInfo = */ createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - it.countDown() } // Create orphan rooms var orphan1 = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) - it.countDown() } var orphan2 = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) - it.countDown() } val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) @@ -240,10 +235,9 @@ class SpaceHierarchyTest : InstrumentedTest { assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) // Add a non canonical child and check that it does not appear as orphan - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) spaceA!!.addChildren(a3, viaServers, null, false) - it.countDown() } Thread.sleep(6_000) @@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest { @Test @Ignore("This test will be ignored until it is fixed") - fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testBreakCycle() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - it.countDown() } // add back A as subspace of C - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) - it.countDown() } // A -> C -> A @@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test - fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> + fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper -> val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, + "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceBInfo = createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, + "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - runBlocking { + commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) } val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, + "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) commonTestHelper.waitWithLatch { latch -> @@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest { } } + flatAChildren.observeForever(childObserver) + // add C as subspace of B val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) // C1 and C2 should be in flatten child of A now - - flatAChildren.observeForever(childObserver) } // Test part one of the rooms @@ -374,10 +379,10 @@ class SpaceHierarchyTest : InstrumentedTest { } } - // part from b room - session.roomService().leaveRoom(bRoomId) // The room should have disapear from flat children flatAChildren.observeForever(childObserver) + // part from b room + session.roomService().leaveRoom(bRoomId) } commonTestHelper.signOutAndClose(session) } @@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest { ) private fun createPublicSpace( + commonTestHelper: CommonTestHelper, session: Session, spaceName: String, childInfo: List> @@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest { ): TestSpaceCreationResult { var spaceId = "" var roomIds: List = emptyList() - runSessionTest(context()) { commonTestHelper -> - commonTestHelper.waitWithLatch { latch -> - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - - roomIds = childInfo.map { entry -> - session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + commonTestHelper.runBlockingTest { + spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + roomIds = childInfo.map { entry -> + session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } - latch.countDown() } } return TestSpaceCreationResult(spaceId, roomIds) } private fun createPrivateSpace( + commonTestHelper: CommonTestHelper, session: Session, spaceName: String, childInfo: List> @@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest { ): TestSpaceCreationResult { var spaceId = "" var roomIds: List = emptyList() - runSessionTest(context()) { commonTestHelper -> - commonTestHelper.waitWithLatch { latch -> - spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - roomIds = - childInfo.map { entry -> - val homeServerCapabilities = session - .homeServerCapabilitiesService() - .getHomeServerCapabilities() - session.roomService().createRoom(CreateRoomParams().apply { - name = entry.first - this.featurePreset = RestrictedRoomPreset( - homeServerCapabilities, - listOf( - RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) - ) - ) - }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + commonTestHelper.runBlockingTest { + spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + roomIds = + childInfo.map { entry -> + val homeServerCapabilities = session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + session.roomService().createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } - latch.countDown() } } return TestSpaceCreationResult(spaceId, roomIds) @@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace( - session, "SpaceA", listOf( - Triple("A1", true /*auto-join*/, true/*canonical*/), - Triple("A2", true, true) - ) + commonTestHelper, + session, "SpaceA", + listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + ) ) val spaceBInfo = createPublicSpace( - session, "SpaceB", listOf( - Triple("B1", true /*auto-join*/, true/*canonical*/), - Triple("B2", true, true), - Triple("B3", true, true) - ) + commonTestHelper, + session, "SpaceB", + listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + ) ) val spaceCInfo = createPublicSpace( - session, "SpaceC", listOf( - Triple("C1", true /*auto-join*/, true/*canonical*/), - Triple("C2", true, true) - ) + commonTestHelper, + session, "SpaceC", + listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + ) ) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") @@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest { runBlocking { val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - Thread.sleep(6_000) } // Thread.sleep(4_000) @@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest { // + C // + c1, c2 - val rootSpaces = commonTestHelper.runBlockingTest { - session.spaceService().getRootSpaceSummaries() + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } + rootSpaces.size == 2 + } } - - assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size) } @Test @@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val spaceAInfo = createPrivateSpace( - aliceSession, "Private Space A", listOf( - Triple("General", true /*suggested*/, true/*canonical*/), - Triple("Random", true, true) - ) + commonTestHelper, + aliceSession, "Private Space A", + listOf( + Triple("General", true /*suggested*/, true/*canonical*/), + Triple("Random", true, true) + ) ) commonTestHelper.runBlockingTest { @@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest { } var bobRoomId = "" - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - it.countDown() } commonTestHelper.runBlockingTest { @@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest { } } - commonTestHelper.waitWithLatch { + commonTestHelper.runBlockingTest { bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - it.countDown() } commonTestHelper.waitWithLatch { latch -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt index 9507ddda657..015cb6a1a28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -38,4 +38,5 @@ data class MXCryptoConfig constructor( * You can limit request only to your sessions by turning this setting to `true` */ val limitRoomKeyRequestsToMyDevices: Boolean = false, -) + + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 638da118049..a5e05f69e07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.internal.crypto.model.SessionInfo interface CryptoService { @@ -84,6 +85,20 @@ interface CryptoService { fun isKeyGossipingEnabled(): Boolean + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun enableShareKeyOnInvite(enable: Boolean) + + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun isShareKeysOnInviteEnabled(): Boolean + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -176,4 +191,9 @@ interface CryptoService { * send, in order to speed up sending of the message. */ fun prepareToEncrypt(roomId: String, callback: MatrixCallback) + + /** + * Share all inbound sessions of the last chunk messages to the provided userId devices. + */ + suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt index 3df4ef7c9a0..664cd00e943 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt @@ -69,5 +69,11 @@ data class ForwardedRoomKeyContent( * private part of this key unless they have done device verification. */ @Json(name = "sender_claimed_ed25519_key") - val senderClaimedEd25519Key: String? = null + val senderClaimedEd25519Key: String? = null, + + /** + * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean? = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt index 0830a566ab8..5b18d29ea0a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt @@ -38,5 +38,12 @@ data class RoomKeyContent( // should be a Long but it is sometimes a double @Json(name = "chain_index") - val chainIndex: Any? = null + val chainIndex: Any? = null, + + /** + * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean? = false + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt index 06069f26462..2b0ea1d8fbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt @@ -48,3 +48,9 @@ enum class RoomHistoryVisibility { */ @Json(name = "joined") JOINED } + +/** + * Room history should be shared only if room visibility is world_readable or shared. + */ +internal fun RoomHistoryVisibility.shouldShareHistory() = + this == RoomHistoryVisibility.WORLD_READABLE || this == RoomHistoryVisibility.SHARED diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index e0bcde22963..850a4379ca0 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction @@ -81,6 +82,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFact import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE +import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -963,8 +965,12 @@ internal class DefaultCryptoService @Inject constructor( private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { if (!event.isStateEvent()) return val eventContent = event.content.toModel() - eventContent?.historyVisibility?.let { - cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) + val historyVisibility = eventContent?.historyVisibility + if (historyVisibility == null) { + cryptoStore.setShouldShareHistory(roomId, false) + } else { + cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) + cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) } } @@ -1111,6 +1117,10 @@ internal class DefaultCryptoService @Inject constructor( override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() + override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled() + + override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable) + /** * Tells whether the client should ever send encrypted messages to unverified devices. * The default value is false. @@ -1335,6 +1345,30 @@ internal class DefaultCryptoService @Inject constructor( } } + override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) { + deviceListManager.downloadKeys(listOf(userId), false) + val userDevices = cryptoStore.getUserDeviceList(userId) + val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo -> + // Get inbound session from sessionId and sessionKey + withContext(coroutineDispatchers.crypto) { + olmDevice.getInboundGroupSession( + sessionId = sessionInfo.sessionId, + senderKey = sessionInfo.senderKey, + roomId = roomId + ).takeIf { it.wrapper.sessionData.sharedHistory } + } + } + + userDevices?.forEach { deviceInfo -> + // Lets share the provided inbound sessions for every user device + sessionToShare.forEach { inboundGroupSession -> + val encryptor = roomEncryptorsStore.get(roomId) + encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo) + Timber.i("## CRYPTO | Sharing inbound session") + } + } + } + /* ========================================================================================== * For test only * ========================================================================================== */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt index e4d322cadd3..ab7cbb74b11 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.Mutex import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber import java.util.Timer @@ -31,7 +31,7 @@ import java.util.TimerTask import javax.inject.Inject internal data class InboundGroupSessionHolder( - val wrapper: OlmInboundGroupSessionWrapper2, + val wrapper: MXInboundMegolmSessionWrapper, val mutex: Mutex = Mutex() ) @@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) - oldValue.wrapper.olmInboundGroupSession?.releaseSession() + oldValue.wrapper.session.releaseSession() } } } @@ -67,7 +67,7 @@ internal class InboundGroupSessionStore @Inject constructor( private val timer = Timer() private var timerTask: TimerTask? = null - private val dirtySession = mutableListOf() + private val dirtySession = mutableListOf() @Synchronized fun clear() { @@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor( @Synchronized fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - dirtySession.remove(old.wrapper) + dirtySession.remove(old) store.removeInboundGroupSession(sessionId, senderKey) sessionCache.remove(CacheKey(sessionId, senderKey)) // release removed session - old.wrapper.olmInboundGroupSession?.releaseSession() + old.wrapper.session.releaseSession() internalStoreGroupSession(new, sessionId, senderKey) } @@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor( private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") // We want to batch this a bit for performances - dirtySession.add(holder.wrapper) + dirtySession.add(holder) if (sessionCache[CacheKey(sessionId, senderKey)] == null) { // first time seen, put it in memory cache while waiting for batch insert @@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor( @Synchronized private fun batchSave() { - val toSave = mutableListOf().apply { addAll(dirtySession) } + val toSave = mutableListOf().apply { addAll(dirtySession) } dirtySession.clear() cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") tryOrNull { - store.storeInboundGroupSessions(toSave) + store.storeInboundGroupSessions(toSave.map { it.wrapper }) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 24b6fd166f3..c4a6488258b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.di.MoshiProvider @@ -38,6 +39,7 @@ import org.matrix.android.sdk.internal.util.convertToUTF8 import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException +import org.matrix.olm.OlmInboundGroupSession import org.matrix.olm.OlmMessage import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmSession @@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor( return MXOutboundSessionInfo( sessionId = sessionId, sharedWithHelper = SharedWithHelper(roomId, sessionId, store), - clock, - restoredOutboundGroupSession.creationTime + clock = clock, + creationTime = restoredOutboundGroupSession.creationTime, + sharedHistory = restoredOutboundGroupSession.sharedHistory ) } return null @@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor( * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. * @param keysClaimed Other keys the sender claims. * @param exportFormat true if the megolm keys are in export format + * @param sharedHistory MSC3061, this key is sharable on invite * @return true if the operation succeeds. */ - fun addInboundGroupSession( - sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean - ): AddSessionResult { - val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean, + sharedHistory: Boolean): AddSessionResult { + val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { + if (exportFormat) { + OlmInboundGroupSession.importSession(sessionKey) + } else { + OlmInboundGroupSession(sessionKey) + } + } + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper // If we have an existing one we should check if the new one is not better if (existingSession != null) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") try { - val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { + val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { // This is quite unexpected, could throw if native was released? Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") - candidateSession.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() // Probably should discard it? } - val newKnownFirstIndex = candidateSession.firstKnownIndex + val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex } // If our existing session is better we keep it if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession.olmInboundGroupSession?.releaseSession() + candidateSession?.releaseSession() return AddSessionResult.NotImported } } @@ -639,36 +649,42 @@ internal class MXOlmDevice @Inject constructor( Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") // sanity check on the new session - val candidateOlmInboundSession = candidateSession.olmInboundGroupSession - if (null == candidateOlmInboundSession) { + if (null == candidateSession) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") return AddSessionResult.NotImported } try { - if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { + if (candidateSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundSession.releaseSession() + candidateSession.releaseSession() return AddSessionResult.NotImported } } catch (e: Throwable) { - candidateOlmInboundSession.releaseSession() + candidateSession.releaseSession() Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") return AddSessionResult.NotImported } - candidateSession.senderKey = senderKey - candidateSession.roomId = roomId - candidateSession.keysClaimed = keysClaimed - candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + val candidateSessionData = InboundGroupSessionData( + senderKey = senderKey, + roomId = roomId, + keysClaimed = keysClaimed, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + sharedHistory = sharedHistory, + ) + val wrapper = MXInboundMegolmSessionWrapper( + candidateSession, + candidateSessionData + ) if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey) } else { - inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey) } - return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) + return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) } /** @@ -677,41 +693,22 @@ internal class MXOlmDevice @Inject constructor( * @param megolmSessionsData the megolm sessions data * @return the successfully imported sessions. */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) for (megolmSessionData in megolmSessionsData) { val sessionId = megolmSessionData.sessionId ?: continue val senderKey = megolmSessionData.senderKey ?: continue val roomId = megolmSessionData.roomId - var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null - - try { - candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - } - - // sanity check - if (candidateSessionToImport?.olmInboundGroupSession == null) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session") - continue - } - - val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession - try { - if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundGroupSession?.releaseSession() - continue - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed") - candidateOlmInboundGroupSession?.releaseSession() + val candidateSessionToImport = try { + MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") continue } + val candidateOlmInboundGroupSession = candidateSessionToImport.session val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper @@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor( sessions.add(candidateSessionToImport) } else { Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } - val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } + val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } + val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex } if (existingFirstKnown == null || candidateFirstKnownIndex == null) { // should not happen? - candidateSessionToImport.olmInboundGroupSession?.releaseSession() + candidateSessionToImport.session.releaseSession() Timber.tag(loggerTag.value) .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") } else { - if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { + if (existingFirstKnown <= candidateFirstKnownIndex) { // Ignore this, keep existing candidateOlmInboundGroupSession.releaseSession() } else { @@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor( ): OlmDecryptionResult { val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) val wrapper = sessionHolder.wrapper - val inboundGroupSession = wrapper.olmInboundGroupSession - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") + val inboundGroupSession = wrapper.session if (roomId != wrapper.roomId) { // Check that the room id matches the original one for the session. This stops // the HS pretending a message was targeting a different room. @@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor( return OlmDecryptionResult( payload, - wrapper.keysClaimed, + wrapper.sessionData.keysClaimed, senderKey, - wrapper.forwardingCurve25519KeyChain + wrapper.sessionData.forwardingCurve25519KeyChain ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt index f6bc9a9148a..ca0bdc8a0eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt @@ -69,5 +69,13 @@ internal data class MegolmSessionData( * Devices which forwarded this session to us (normally empty). */ @Json(name = "forwarding_curve25519_key_chain") - val forwardingCurve25519KeyChain: List? = null + val forwardingCurve25519KeyChain: List? = null, + + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. + */ + // When this feature lands in spec name = shared_history should be used + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt index 6b22cc09d6e..810699d9339 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt @@ -437,7 +437,10 @@ internal class OutgoingKeyRequestManager @Inject constructor( if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { // let's see what's the index val knownIndex = tryOrNull { - inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "") + ?.wrapper + ?.session + ?.firstKnownIndex } if (knownIndex != null && knownIndex <= request.fromIndex) { // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index f6ab96aee68..a624b92a198 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -84,8 +84,9 @@ internal class MegolmSessionDataImporter @Inject constructor( megolmSessionData.senderKey ?: "", tryOrNull { olmInboundGroupSessionWrappers - .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } - ?.firstKnownIndex?.toInt() + .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId } + ?.session?.firstKnownIndex + ?.toInt() } ?: 0 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt index 73ce5a5004b..1454f5b4868 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt @@ -16,7 +16,9 @@ package org.matrix.android.sdk.internal.crypto.algorithms +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder /** * An interface for encrypting data. @@ -32,4 +34,6 @@ internal interface IMXEncrypting { * @return the encrypted content */ suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content + + suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {} } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 141d6f74cd0..410b74e19f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener @@ -41,6 +42,7 @@ internal class MXMegolmDecryption( private val olmDevice: MXOlmDevice, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, + private val matrixConfiguration: MatrixConfiguration, private val liveEventManager: Lazy ) : IMXDecrypting { @@ -240,13 +242,14 @@ internal class MXMegolmDecryption( Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") val addSessionResult = olmDevice.addInboundGroupSession( - roomKeyContent.sessionId, - roomKeyContent.sessionKey, - roomKeyContent.roomId, - senderKey, - forwardingCurve25519KeyChain, - keysClaimed, - exportFormat + sessionId = roomKeyContent.sessionId, + sessionKey = roomKeyContent.sessionKey, + roomId = roomKeyContent.roomId, + senderKey = senderKey, + forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, + keysClaimed = keysClaimed, + exportFormat = exportFormat, + sharedHistory = roomKeyContent.getSharedKey() ) when (addSessionResult) { @@ -296,6 +299,14 @@ internal class MXMegolmDecryption( } } + /** + * Returns boolean shared key flag, if enabled with respect to matrix configuration. + */ + private fun RoomKeyContent.getSharedKey(): Boolean { + if (!cryptoStore.isShareKeysOnInviteEnabled()) return false + return sharedHistory ?: false + } + /** * Check if the some messages can be decrypted with a new session. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 81a6fb28c06..414416a0f6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( private val olmDevice: MXOlmDevice, private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, + private val matrixConfiguration: MatrixConfiguration, private val eventsManager: Lazy ) { @@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( olmDevice, outgoingKeyRequestManager, cryptoStore, - eventsManager - ) + matrixConfiguration, + eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 7bfbae6edf5..48a25f2a8b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter @@ -151,14 +152,27 @@ internal class MXMegolmEncryption( "ed25519" to olmDevice.deviceEd25519Key!! ) + val sharedHistory = cryptoStore.shouldShareHistory(roomId) + Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory") olmDevice.addInboundGroupSession( - sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, - emptyList(), keysClaimedMap, false + sessionId = sessionId!!, + sessionKey = olmDevice.getSessionKey(sessionId)!!, + roomId = roomId, + senderKey = olmDevice.deviceCurve25519Key!!, + forwardingCurve25519KeyChain = emptyList(), + keysClaimed = keysClaimedMap, + exportFormat = false, + sharedHistory = sharedHistory ) defaultKeysBackupService.maybeBackupKeys() - return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock) + return MXOutboundSessionInfo( + sessionId = sessionId, + sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore), + clock = clock, + sharedHistory = sharedHistory + ) } /** @@ -172,6 +186,8 @@ internal class MXMegolmEncryption( if (session == null || // Need to make a brand new session? session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || + // Is there a room history visibility change since the last outboundSession + cryptoStore.shouldShareHistory(roomId) != session.sharedHistory || // Determine if we have shared with anyone we shouldn't have session.sharedWithTooManyDevices(devicesInRoom)) { Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") @@ -231,26 +247,27 @@ internal class MXMegolmEncryption( /** * Share the device keys of a an user. * - * @param session the session info + * @param sessionInfo the session info * @param devicesByUser the devices map */ - private suspend fun shareUserDevicesKey( - session: MXOutboundSessionInfo, - devicesByUser: Map> - ) { - val sessionKey = olmDevice.getSessionKey(session.sessionId) - val chainIndex = olmDevice.getMessageIndex(session.sessionId) - - val submap = HashMap() - submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM - submap["room_id"] = roomId - submap["session_id"] = session.sessionId - submap["session_key"] = sessionKey!! - submap["chain_index"] = chainIndex - - val payload = HashMap() - payload["type"] = EventType.ROOM_KEY - payload["content"] = submap + private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo, + devicesByUser: Map>) { + val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { + Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") + } + val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId) + + val payload = mapOf( + "type" to EventType.ROOM_KEY, + "content" to mapOf( + "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM, + "room_id" to roomId, + "session_id" to sessionInfo.sessionId, + "session_key" to sessionKey, + "chain_index" to chainIndex, + "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory + ) + ) var t0 = clock.epochMillis() Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") @@ -292,7 +309,7 @@ internal class MXMegolmEncryption( // for dead devices on every message. for ((_, devicesToShareWith) in devicesByUser) { for (deviceInfo in devicesToShareWith) { - session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) + sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) // XXX is it needed to add it to the audit trail? // For now decided that no, we are more interested by forward trail } @@ -300,8 +317,8 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = clock.epochMillis() - Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") - Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target") + Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { withContext(coroutineDispatchers.io) { @@ -310,7 +327,7 @@ internal class MXMegolmEncryption( Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>") + Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>") } } else { Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") @@ -320,7 +337,7 @@ internal class MXMegolmEncryption( // XXX offload?, as they won't read the message anyhow? notifyKeyWithHeld( noOlmToNotify, - session.sessionId, + sessionInfo.sessionId, olmDevice.deviceCurve25519Key, WithHeldCode.NO_OLM ) @@ -514,6 +531,51 @@ internal class MXMegolmEncryption( } } + @Throws + override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) { + if (!inboundSessionWrapper.wrapper.sessionData.sharedHistory) throw IllegalArgumentException("This key can't be shared") + Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") + val userId = deviceInfo.userId + val deviceId = deviceInfo.deviceId + val devicesByUser = mapOf(userId to listOf(deviceInfo)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm") + // process anyway? + null + } + val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys") + return + } + + val export = inboundSessionWrapper.mutex.withLock { + inboundSessionWrapper.wrapper.exportKeys() + } ?: return Unit.also { + Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}") + } + + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) + + val encodedPayload = + withContext(coroutineDispatchers.computation) { + messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + } + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.tag(loggerTag.value) + .d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(sendToDeviceParams) + } + } + data class DeviceInRoomInfo( val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 28d925d8fd5..e0caa0d9a59 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo( private val clock: Clock, // When the session was created private val creationTime: Long = clock.epochMillis(), + val sharedHistory: Boolean = false ) { // Number of times this session has been used diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 5eaa106af3c..49cf60d0518 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP @@ -50,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.ObjectSigner @@ -71,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.di.MoshiProvider @@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor( private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor private val taskExecutor: TaskExecutor, + private val matrixConfiguration: MatrixConfiguration, + private val inboundGroupSessionStore: InboundGroupSessionStore, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : KeysBackupService { @@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor( olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach - val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach + val olmInboundGroupSession = olmInboundGroupSessionWrapper.session try { encryptGroupSession(olmInboundGroupSessionWrapper) @@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor( @VisibleForTesting @WorkerThread - fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? { + suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? { + olmInboundGroupSessionWrapper.safeSessionId ?: return null + olmInboundGroupSessionWrapper.senderKey ?: return null // Gather information for each key - val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) } + val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey) // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format - val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null + val sessionData = inboundGroupSessionStore + .getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey) + ?.let { + withContext(coroutineDispatchers.computation) { + it.mutex.withLock { it.wrapper.exportKeys() } + } + } + ?: return null val sessionBackupData = mapOf( "algorithm" to sessionData.algorithm, "sender_key" to sessionData.senderKey, "sender_claimed_keys" to sessionData.senderClaimedKeys, "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), - "session_key" to sessionData.sessionKey + "session_key" to sessionData.sessionKey, + "org.matrix.msc3061.shared_history" to sessionData.sharedHistory ) val json = MoshiProvider.providesMoshi() @@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor( .toJson(sessionBackupData) val encryptedSessionBackupData = try { - backupOlmPkEncryption?.encrypt(json) + withContext(coroutineDispatchers.computation) { + backupOlmPkEncryption?.encrypt(json) + } } catch (e: OlmException) { Timber.e(e, "OlmException") null @@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor( // Build backup data for that key return KeyBackupData( firstMessageIndex = try { - olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0 + olmInboundGroupSessionWrapper.session.firstKnownIndex } catch (e: OlmException) { Timber.e(e, "OlmException") 0L }, - forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size, + forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size, isVerified = device?.isVerified == true, - + sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(), sessionData = mapOf( "ciphertext" to encryptedSessionBackupData.mCipherText, "mac" to encryptedSessionBackupData.mMac, @@ -1451,6 +1468,14 @@ internal class DefaultKeysBackupService @Inject constructor( ) } + /** + * Returns boolean shared key flag, if enabled with respect to matrix configuration. + */ + private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean { + if (!cryptoStore.isShareKeysOnInviteEnabled()) return false + return sessionData.sharedHistory + } + @VisibleForTesting @WorkerThread fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt index 5c3d0c12b0e..1817b18e2ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -50,5 +50,12 @@ internal data class KeyBackupData( * Algorithm-dependent data. */ @Json(name = "session_data") - val sessionData: JsonDict + val sessionData: JsonDict, + + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. + */ + @Json(name = "org.matrix.msc3061.shared_history") + val sharedHistory: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt new file mode 100644 index 00000000000..2ce36aa209b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InboundGroupSessionData( + + /** The room in which this session is used. */ + @Json(name = "room_id") + var roomId: String? = null, + + /** The base64-encoded curve25519 key of the sender. */ + @Json(name = "sender_key") + var senderKey: String? = null, + + /** Other keys the sender claims. */ + @Json(name = "keys_claimed") + var keysClaimed: Map? = null, + + /** Devices which forwarded this session to us (normally emty). */ + @Json(name = "forwarding_curve25519_key_chain") + var forwardingCurve25519KeyChain: List? = emptyList(), + + /** Not yet used, will be in backup v2 + val untrusted?: Boolean = false */ + + /** + * Flag that indicates whether or not the current inboundSession will be shared to + * invited users to decrypt past messages. + */ + @Json(name = "shared_history") + val sharedHistory: Boolean = false, + + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt new file mode 100644 index 00000000000..2772b348359 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber + +data class MXInboundMegolmSessionWrapper( + // olm object + val session: OlmInboundGroupSession, + // data about the session + val sessionData: InboundGroupSessionData +) { + // shortcut + val roomId = sessionData.roomId + val senderKey = sessionData.senderKey + val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() } + + /** + * 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 + */ + internal fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + val keysClaimed = sessionData.keysClaimed ?: return null + val wantedIndex = index ?: session.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(), + sessionKey = session.export(wantedIndex), + senderClaimedKeys = keysClaimed, + roomId = sessionData.roomId, + sessionId = session.sessionIdentifier(), + senderKey = senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sharedHistory = sessionData.sharedHistory + ) + } catch (e: Exception) { + Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed") + null + } + } + + companion object { + + /** + * @exportFormat true if the megolm keys are in export format + * (ie, they lack an ed25519 signature) + */ + @Throws + internal fun newFromMegolmData(megolmSessionData: MegolmSessionData, exportFormat: Boolean): MXInboundMegolmSessionWrapper { + val exportedKey = megolmSessionData.sessionKey ?: throw IllegalArgumentException("key data not found") + val inboundSession = if (exportFormat) { + OlmInboundGroupSession.importSession(exportedKey) + } else { + OlmInboundGroupSession(exportedKey) + } + .also { + if (it.sessionIdentifier() != megolmSessionData.sessionId) { + it.releaseSession() + throw IllegalStateException("Mismatched group session Id") + } + } + val data = InboundGroupSessionData( + roomId = megolmSessionData.roomId, + senderKey = megolmSessionData.senderKey, + keysClaimed = megolmSessionData.senderClaimedKeys, + forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, + sharedHistory = megolmSessionData.sharedHistory, + ) + + return MXInboundMegolmSessionWrapper( + inboundSession, + data + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt index 289c169d6d1..600fcb10033 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt @@ -26,6 +26,8 @@ import java.io.Serializable * This class adds more context to a OlmInboundGroupSession object. * This allows additional checks. The class implements Serializable so that the context can be stored. */ +// Note used anymore, just for database migration +// Deprecated("Use MXInboundMegolmSessionWrapper") internal class OlmInboundGroupSessionWrapper2 : Serializable { // The associated olm inbound group session. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt index 4ac87f44cef..5a6d1f4bc12 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt @@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession internal data class OutboundGroupSessionWrapper( val outboundGroupSession: OlmOutboundGroupSession, - val creationTime: Long + val creationTime: Long, + /** + * As per MSC 3061, declares if this key could be shared when inviting a new user to the room. + */ + val sharedHistory: Boolean = false ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt new file mode 100644 index 00000000000..b3a2ba4dfe7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model + +data class SessionInfo( + val sessionId: String, + val senderKey: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index b5b8d8e9744..0413fc730c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -35,7 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity @@ -64,7 +64,15 @@ internal interface IMXCryptoStore { * * @return the list of all known group sessions, to export them. */ - fun getInboundGroupSessions(): List + fun getInboundGroupSessions(): List + + /** + * Retrieve the known inbound group sessions for the specified room. + * + * @param roomId The roomId that the sessions will be returned + * @return the list of all known group sessions, for the provided roomId + */ + fun getInboundGroupSessions(roomId: String): List /** * @return true to unilaterally blacklist all unverified devices. @@ -90,6 +98,20 @@ internal interface IMXCryptoStore { fun isKeyGossipingEnabled(): Boolean + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun enableShareKeyOnInvite(enable: Boolean) + + /** + * As per MSC3061. + * If true will make it possible to share part of e2ee room history + * on invite depending on the room visibility setting. + */ + fun isShareKeysOnInviteEnabled(): Boolean + /** * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. * @@ -250,6 +272,17 @@ internal interface IMXCryptoStore { fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + fun shouldShareHistory(roomId: String): Boolean + + /** + * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) + * will be shared to new user invites. + * + * @param roomId the room id + * @param shouldShareHistory The boolean flag + */ + fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) + /** * Store a session between the logged-in user and another device. * @@ -290,7 +323,7 @@ internal interface IMXCryptoStore { * * @param sessions the inbound group sessions to store. */ - fun storeInboundGroupSessions(sessions: List) + fun storeInboundGroupSessions(sessions: List) /** * Retrieve an inbound group session. @@ -299,7 +332,17 @@ internal interface IMXCryptoStore { * @param senderKey the base64-encoded curve25519 key of the sender. * @return an inbound group session. */ - fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? + fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? + + /** + * Retrieve an inbound group session, filtering shared history. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param sharedHistory filter inbound session with respect to shared history field + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? /** * Get the current outbound group session for this encrypted room. @@ -333,7 +376,7 @@ internal interface IMXCryptoStore { * * @param olmInboundGroupSessionWrappers the sessions */ - fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) + fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) /** * Retrieve inbound group sessions that are not yet backed up. @@ -341,7 +384,7 @@ internal interface IMXCryptoStore { * @param limit the maximum number of sessions to return. * @return an array of non backed up inbound group sessions. */ - fun inboundGroupSessionsToBackup(limit: Int): List + fun inboundGroupSessionsToBackup(limit: Int): List /** * Number of stored inbound group sessions. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 028d8f73f95..20ca357d1ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldCo import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor( ?: false } + override fun shouldShareHistory(roomId: String): Boolean { + if (!isShareKeysOnInviteEnabled()) return false + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory + } + ?: false + } + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers } } + override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room $roomId is $shouldShareHistory") + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory + } + } + override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { var sessionIdentifier: String? = null @@ -727,54 +743,55 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun storeInboundGroupSessions(sessions: List) { + override fun storeInboundGroupSessions(sessions: List) { if (sessions.isEmpty()) { return } doRealmTransaction(realmConfiguration) { realm -> - sessions.forEach { session -> - var sessionIdentifier: String? = null + sessions.forEach { wrapper -> - try { - sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier() + val sessionIdentifier = try { + wrapper.session.sessionIdentifier() } catch (e: OlmException) { Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") + return@forEach } - if (sessionIdentifier != null) { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) +// val shouldShareHistory = session.roomId?.let { roomId -> +// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory +// } ?: false + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - if (existing != null) { - // we want to keep the existing backup status - existing.putInboundGroupSession(session) - } else { - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - senderKey = session.senderKey - putInboundGroupSession(session) - } - - realm.insertOrUpdate(realmOlmInboundGroupSession) - } + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + store(wrapper) } + Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") + realm.insertOrUpdate(realmOlmInboundGroupSession) } } } - override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.toModel() + } + } + + override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) return doWithRealm(realmConfiguration) { it.where() + .equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory) .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() - ?.getInboundGroupSession() + ?.toModel() } } @@ -786,7 +803,8 @@ internal class RealmCryptoStore @Inject constructor( entity.getOutboundGroupSession()?.let { OutboundGroupSessionWrapper( it, - entity.creationTime ?: 0 + entity.creationTime ?: 0, + entity.shouldShareHistory ) } } @@ -806,6 +824,8 @@ internal class RealmCryptoStore @Inject constructor( if (outboundGroupSession != null) { val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { creationTime = clock.epochMillis() + // Store the room history visibility on the outbound session creation + shouldShareHistory = entity.shouldShareHistory putOutboundGroupSession(outboundGroupSession) } entity.outboundSessionInfo = info @@ -814,17 +834,32 @@ internal class RealmCryptoStore @Inject constructor( } } +// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean { +// return doWithRealm(realmConfiguration) { realm -> +// CryptoRoomEntity.getById(realm, roomId)?.let { entity -> +// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory +// } +// } ?: false +// } + /** * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management. */ - override fun getInboundGroupSessions(): List { - return doWithRealm(realmConfiguration) { - it.where() + override fun getInboundGroupSessions(): List { + return doWithRealm(realmConfiguration) { realm -> + realm.where() .findAll() - .mapNotNull { inboundGroupSessionEntity -> - inboundGroupSessionEntity.getInboundGroupSession() - } + .mapNotNull { it.toModel() } + } + } + + override fun getInboundGroupSessions(roomId: String): List { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId) + .findAll() + .mapNotNull { it.toModel() } } } @@ -885,7 +920,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { + override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { if (olmInboundGroupSessionWrappers.isEmpty()) { return } @@ -893,10 +928,13 @@ internal class RealmCryptoStore @Inject constructor( doRealmTransaction(realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { - val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() + val sessionIdentifier = + tryOrNull("Failed to get session identifier") { + olmInboundGroupSessionWrapper.session.sessionIdentifier() + } ?: return@forEach val key = OlmInboundGroupSessionEntity.createPrimaryKey( sessionIdentifier, - olmInboundGroupSessionWrapper.senderKey + olmInboundGroupSessionWrapper.sessionData.senderKey ) val existing = realm.where() @@ -909,9 +947,7 @@ internal class RealmCryptoStore @Inject constructor( // ... might be in cache but not yet persisted, create a record to persist backedup state val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { primaryKey = key - sessionId = sessionIdentifier - senderKey = olmInboundGroupSessionWrapper.senderKey - putInboundGroupSession(olmInboundGroupSessionWrapper) + store(olmInboundGroupSessionWrapper) backedUp = true } @@ -924,15 +960,13 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun inboundGroupSessionsToBackup(limit: Int): List { + override fun inboundGroupSessionsToBackup(limit: Int): List { return doWithRealm(realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) .limit(limit.toLong()) .findAll() - .mapNotNull { inboundGroupSession -> - inboundGroupSession.getInboundGroupSession() - } + .mapNotNull { it.toModel() } } } @@ -973,6 +1007,18 @@ internal class RealmCryptoStore @Inject constructor( } ?: false } + override fun isShareKeysOnInviteEnabled(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.enableKeyForwardingOnInvite + } ?: false + } + + override fun enableShareKeyOnInvite(enable: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.enableKeyForwardingOnInvite = enable + } + } + override fun setDeviceKeysUploaded(uploaded: Boolean) { doRealmTransaction(realmConfiguration) { it.where().findFirst()?.deviceKeysSentToServer = uploaded diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 02c2a27decd..4ca9d44f98d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -51,7 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - val schemaVersion = 16L + val schemaVersion = 17L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -72,5 +73,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 16) MigrateCryptoTo016(realm).perform() + if (oldVersion < 17) MigrateCryptoTo017(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt new file mode 100644 index 00000000000..8904c412cd8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import timber.log.Timber + +/** + * Version 17L enhance OlmInboundGroupSessionEntity to support shared history for MSC3061. + * Also migrates how megolm session are stored to avoid additional serialized frozen class. + */ +internal class MigrateCryptoTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform { + // We don't have access to the session database to check for the state here and set the good value. + // But for now as it's behind a lab flag, will set to false and force initial sync when enabled + it.setBoolean(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, false) + } + + realm.schema.get("OutboundGroupSessionInfoEntity") + ?.addField(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform { + // We don't have access to the session database to check for the state here and set the good value. + // But for now as it's behind a lab flag, will set to false and force initial sync when enabled + it.setBoolean(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, false) + } + + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, Boolean::class.java) + ?.transform { obj -> + // default to false + obj.setBoolean(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, false) + } + + val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java) + + realm.schema.get("OlmInboundGroupSessionEntity") + ?.addField(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, Boolean::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.ROOM_ID, String::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, String::class.java) + ?.addField(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, String::class.java) + ?.transform { dynamicObject -> + try { + // we want to convert the old wrapper frozen class into a + // map of sessionData & the pickled session herself + dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { oldData -> + val oldWrapper = tryOrNull("Failed to convert megolm inbound group data") { + @Suppress("DEPRECATION") + deserializeFromRealm(oldData) + } + val groupSession = oldWrapper?.olmInboundGroupSession + ?: return@transform Unit.also { + Timber.w("Failed to migrate megolm session, no olmInboundGroupSession") + } + // now convert to new data + val data = InboundGroupSessionData( + senderKey = oldWrapper.senderKey, + roomId = oldWrapper.roomId, + keysClaimed = oldWrapper.keysClaimed, + forwardingCurve25519KeyChain = oldWrapper.forwardingCurve25519KeyChain, + sharedHistory = false, + ) + + dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(data)) + dynamicObject.setString(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, serializeForRealm(groupSession)) + + // denormalized fields + dynamicObject.setString(OlmInboundGroupSessionEntityFields.ROOM_ID, oldWrapper.roomId) + dynamicObject.setBoolean(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, false) + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to migrate megolm session") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 63ed0e537e4..88708f824ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -35,6 +35,11 @@ internal open class CryptoMetadataEntity( var globalBlacklistUnverifiedDevices: Boolean = false, // setting to enable or disable key gossiping var globalEnableKeyGossiping: Boolean = true, + + // MSC3061: Sharing room keys for past messages + // If set to true key history will be shared to invited users with respect to room setting + var enableKeyForwardingOnInvite: Boolean = false, + // The keys backup version currently used. Null means no backup. var backupVersion: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt index 114a596964f..be575861632 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -24,6 +24,8 @@ internal open class CryptoRoomEntity( var algorithm: String? = null, var shouldEncryptForInvitedMembers: Boolean? = null, var blacklistUnverifiedDevices: Boolean = false, + // Determines whether or not room history should be shared on new member invites + var shouldShareHistory: Boolean = false, // Store the current outbound session for this room, // to avoid re-create and re-share at each startup (if rotation not needed..) // This is specific to megolm but not sure how to model it better diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index a4f6c279acb..62ab73e379e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.model import io.realm.RealmObject import io.realm.annotations.PrimaryKey -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.olm.OlmInboundGroupSession import timber.log.Timber internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" @@ -28,27 +31,83 @@ internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: internal open class OlmInboundGroupSessionEntity( // Combined value to build a primary key @PrimaryKey var primaryKey: String? = null, + + // denormalization for faster querying (these fields are in the inboundGroupSessionDataJson) var sessionId: String? = null, var senderKey: String? = null, - // olmInboundGroupSessionData contains Json + var roomId: String? = null, + + // Deprecated, used for migration / olmInboundGroupSessionData contains Json + // keep it in case of problem to have a chance to recover var olmInboundGroupSessionData: String? = null, + + // Stores the session data in an extensible format + // to allow to store data not yet supported for later use + var inboundGroupSessionDataJson: String? = null, + + // The pickled session + var serializedOlmInboundGroupSession: String? = null, + + // Flag that indicates whether or not the current inboundSession will be shared to + // invited users to decrypt past messages + var sharedHistory: Boolean = false, // Indicate if the key has been backed up to the homeserver var backedUp: Boolean = false ) : RealmObject() { - fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { + fun store(wrapper: MXInboundMegolmSessionWrapper) { + this.serializedOlmInboundGroupSession = serializeForRealm(wrapper.session) + this.inboundGroupSessionDataJson = adapter.toJson(wrapper.sessionData) + this.roomId = wrapper.sessionData.roomId + this.senderKey = wrapper.sessionData.senderKey + this.sessionId = wrapper.session.sessionIdentifier() + this.sharedHistory = wrapper.sessionData.sharedHistory + } +// fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { +// return try { +// deserializeFromRealm(olmInboundGroupSessionData) +// } catch (failure: Throwable) { +// Timber.e(failure, "## Deserialization failure") +// return null +// } +// } +// +// fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { +// olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) +// } + + fun getOlmGroupSession(): OlmInboundGroupSession? { return try { - deserializeFromRealm(olmInboundGroupSessionData) + deserializeFromRealm(serializedOlmInboundGroupSession) } catch (failure: Throwable) { Timber.e(failure, "## Deserialization failure") return null } } - fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { - olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) + fun getData(): InboundGroupSessionData? { + return try { + inboundGroupSessionDataJson?.let { + adapter.fromJson(it) + } + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } } - companion object + fun toModel(): MXInboundMegolmSessionWrapper? { + val data = getData() ?: return null + val session = getOlmGroupSession() ?: return null + return MXInboundMegolmSessionWrapper( + session = session, + sessionData = data + ) + } + + companion object { + private val adapter = MoshiProvider.providesMoshi() + .adapter(InboundGroupSessionData::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt index d50db78415d..2ebd550201f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt @@ -24,7 +24,8 @@ import timber.log.Timber internal open class OutboundGroupSessionInfoEntity( var serializedOutboundSessionData: String? = null, - var creationTime: Long? = null + var creationTime: Long? = null, + var shouldShareHistory: Boolean = false ) : RealmObject() { fun getOutboundGroupSession(): OlmOutboundGroupSession? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index fbd9d245d9c..bb14b417dd3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor( params.event.roomId ?.takeIf { params.encrypt } ?.let { roomId -> - tryOrNull { + try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } catch (failure: Throwable) { + // send any way? + // the result is that some users won't probably be able to decrypt :/ + Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 234caec970d..221abe0df50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -18,7 +18,11 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.kotlin.createObject +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.crypto.model.SessionInfo +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity @@ -31,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.find +import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection @@ -180,3 +185,12 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean { // We don't know, so we assume it's false return false } + +internal fun ChunkEntity.Companion.findLatestSessionInfo(realm: Realm, roomId: String): Set? = + ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.mapNotNull { timelineEvent -> + timelineEvent?.root?.asDomain()?.content?.toModel()?.let { content -> + content.sessionId ?: return@mapNotNull null + content.senderKey ?: return@mapNotNull null + SessionInfo(content.sessionId, content.senderKey) + } + }?.toSet() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index b5b46a3f5ae..113e780e5ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import okhttp3.ConnectionSpec +import okhttp3.Dispatcher import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.logging.HttpLoggingInterceptor @@ -73,7 +74,9 @@ internal object NetworkModule { apiInterceptor: ApiInterceptor ): OkHttpClient { val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build() - + val dispatcher = Dispatcher().apply { + maxRequestsPerHost = 20 + } return OkHttpClient.Builder() // workaround for #4669 .protocols(listOf(Protocol.HTTP_1_1)) @@ -94,6 +97,7 @@ internal object NetworkModule { addInterceptor(curlLoggingInterceptor) } } + .dispatcher(dispatcher) .connectionSpecs(Collections.singletonList(spec)) .applyMatrixConfiguration(matrixConfiguration) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt index f75fb017466..90d2719e250 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt @@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con override fun register(hasChanged: () -> Unit) { hasChangedCallback = hasChanged - conn.registerDefaultNetworkCallback(networkCallback) + // Add a try catch for safety + // XXX: It happens when running all tests in CI, at some points we reach a limit here causing TooManyRequestsException + // and crashing the sync thread. We might have problem here, would need some investigation + // for now adding a catch to allow CI to continue running + try { + conn.registerDefaultNetworkCallback(networkCallback) + } catch (t: Throwable) { + Timber.e(t, "Unable to register default network callback") + } } override fun unregister() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index ec140e7b171..20708b38142 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -17,18 +17,23 @@ package org.matrix.android.sdk.internal.session.room.membership import androidx.lifecycle.LiveData +import com.otaliastudios.opengl.core.use import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm import io.realm.RealmQuery +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.internal.database.helper.findLatestSessionInfo import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType @@ -50,8 +55,10 @@ internal class DefaultMembershipService @AssistedInject constructor( private val inviteThreePidTask: InviteThreePidTask, private val membershipAdminTask: MembershipAdminTask, private val roomDataSource: RoomDataSource, + private val cryptoService: CryptoService, @UserId private val userId: String, + private val matrixConfiguration: MatrixConfiguration, private val queryStringValueProcessor: QueryStringValueProcessor ) : MembershipService { @@ -139,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor( } override suspend fun invite(userId: String, reason: String?) { + sendShareHistoryKeysIfNeeded(userId) val params = InviteTask.Params(roomId, userId, reason) inviteTask.execute(params) } + private suspend fun sendShareHistoryKeysIfNeeded(userId: String) { + if (!cryptoService.isShareKeysOnInviteEnabled()) return + // TODO not sure it's the right way to get the latest messages in a room + val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use { + ChunkEntity.findLatestSessionInfo(it, roomId) + } + cryptoService.sendSharedHistoryKeys(roomId, userId, sessionInfo) + } + override suspend fun invite3pid(threePid: ThreePid) { val params = InviteThreePidTask.Params(roomId, threePid) return inviteThreePidTask.execute(params) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 6d91dc33484..ea039f0ed82 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -168,6 +168,8 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" private const val SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY = "SETTINGS_DEVELOPER_MODE_SHOW_INFO_ON_SCREEN_KEY" + const val SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY = "SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY" + // SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM" const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 3b9fdc5e55b..70908d75606 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.preference.Preference +import androidx.preference.SwitchPreference import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.preference.VectorSwitchPreference @@ -57,6 +58,17 @@ class VectorSettingsLabsFragment @Inject constructor( false } } + + findPreference(VectorPreferences.SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY)?.let { pref -> + // ensure correct default + pref.isChecked = session.cryptoService().isShareKeysOnInviteEnabled() + + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + session.cryptoService().enableShareKeyOnInvite(pref.isChecked) + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) + true + } + } } /** diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 933f3f06026..bc1b59ebd4b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2958,6 +2958,9 @@ Enable LaTeX mathematics Restart the application for the change to take effect. + MSC3061: Sharing room keys for past messages + When inviting in an encrypted room that is sharing history, encrypted history will be visible. + Create Poll Poll question or topic diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 9b9293e2e26..555470e6433 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -45,6 +45,13 @@ android:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB" android:title="@string/labs_show_unread_notifications_as_tab" /> + +