Merge pull request #5853 from vector-im/feature/aris/crypto_share_room_keys_past_messages

Share Megolm session keys when inviting a new user
This commit is contained in:
Valere 2022-07-01 17:33:43 +02:00 committed by GitHub
commit 8dc57fe2f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1844 additions and 397 deletions

1
changelog.d/5853.feature Normal file
View File

@ -0,0 +1 @@
Improve user experience when he is first invited to a room. Users will be able to decrypt and view previous messages

View File

@ -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 <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {

View File

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

View File

@ -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<TimelineEvent>, List<TimelineEvent>, 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<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = commonTestHelper.doSync<KeysVersion> {
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<KeysVersionResult?> {
kbs.getVersion(version.version, it)
}
val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> {
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 })
}
}

View File

@ -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<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId ->

View File

@ -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<RoomHistoryVisibilityContent>()?.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<RoomHistoryVisibilityContent>()
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<Session>, 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
}
}
}
}

View File

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

View File

@ -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<OlmInboundGroupSessionWrapper2>,
val aliceKeys: List<MXInboundMegolmSessionWrapper>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session
) {

View File

@ -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!!
)

View File

@ -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())
}

View File

@ -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")

View File

@ -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<Triple<String, Boolean, Boolean?>>
@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult {
var spaceId = ""
var roomIds: List<String> = 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 ?: "")
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 = 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)
}
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<Triple<String, Boolean, Boolean?>>
@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult {
var spaceId = ""
var roomIds: List<String> = 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 ->

View File

@ -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,
)
)

View File

@ -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<Unit>)
/**
* Share all inbound sessions of the last chunk messages to the provided userId devices.
*/
suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?)
}

View File

@ -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,
)

View File

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

View File

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

View File

@ -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<RoomHistoryVisibilityContent>()
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<SessionInfo>?) {
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
* ========================================================================================== */

View File

@ -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<OlmInboundGroupSessionWrapper2>()
private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
@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<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
val toSave = mutableListOf<InboundGroupSessionHolder>().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 })
}
}
}

View File

@ -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<String>,
keysClaimed: Map<String, String>,
exportFormat: Boolean
): AddSessionResult {
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
fun addInboundGroupSession(sessionId: String,
sessionKey: String,
roomId: String,
senderKey: String,
forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>,
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 <null>")
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<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
val sessions = ArrayList<MXInboundMegolmSessionWrapper>(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
)
}

View File

@ -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<String>? = null
val forwardingCurve25519KeyChain: List<String>? = 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,
)

View File

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

View File

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

View File

@ -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<String>): Content
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
}

View File

@ -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<StreamEventsManager>
) : 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.
*

View File

@ -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<StreamEventsManager>
) {
@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
olmDevice,
outgoingKeyRequestManager,
cryptoStore,
eventsManager
)
matrixConfiguration,
eventsManager)
}
}

View File

@ -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<String, List<CryptoDeviceInfo>>
) {
val sessionKey = olmDevice.getSessionKey(session.sessionId)
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
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 submap = HashMap<String, Any>()
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<String, Any>()
payload["type"] = EventType.ROOM_KEY
payload["content"] = submap
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<Any>()
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<CryptoDeviceInfo> = MXUsersDevicesMap(),
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()

View File

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

View File

@ -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? {

View File

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

View File

@ -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<String, String>? = null,
/** Devices which forwarded this session to us (normally emty). */
@Json(name = "forwarding_curve25519_key_chain")
var forwardingCurve25519KeyChain: List<String>? = 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,
)

View File

@ -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
)
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<OlmInboundGroupSessionWrapper2>
fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
/**
* 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<MXInboundMegolmSessionWrapper>
/**
* @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<OlmInboundGroupSessionWrapper2>)
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
/**
* 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<OlmInboundGroupSessionWrapper2>)
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
/**
* 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<OlmInboundGroupSessionWrapper2>
fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
/**
* Number of stored inbound group sessions.

View File

@ -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<OlmInboundGroupSessionWrapper2>) {
override fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>) {
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<OlmInboundGroupSessionEntity>()
.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) {
it.where<OlmInboundGroupSessionEntity>()
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
?.getInboundGroupSession()
?.toModel()
}
}
override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory)
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
?.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<OlmInboundGroupSessionWrapper2> {
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.findAll()
.mapNotNull { inboundGroupSessionEntity ->
inboundGroupSessionEntity.getInboundGroupSession()
}
.mapNotNull { it.toModel() }
}
}
override fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId)
.findAll()
.mapNotNull { it.toModel() }
}
}
@ -885,7 +920,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) {
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>) {
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<OlmInboundGroupSessionEntity>()
@ -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<OlmInboundGroupSessionWrapper2> {
override fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
.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<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite
} ?: false
}
override fun enableShareKeyOnInvite(enable: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable
}
}
override fun setDeviceKeysUploaded(uploaded: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded

View File

@ -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()
}
}

View File

@ -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<org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2?>(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")
}
}
}
}

View File

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

View File

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

View File

@ -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<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
// } catch (failure: Throwable) {
// Timber.e(failure, "## Deserialization failure")
// return null
// }
// }
//
// fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
// olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
// }
fun getOlmGroupSession(): OlmInboundGroupSession? {
return try {
deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(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)
}
}

View File

@ -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? {

View File

@ -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}")
}
}

View File

@ -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<SessionInfo>? =
ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.mapNotNull { timelineEvent ->
timelineEvent?.root?.asDomain()?.content?.toModel<EncryptedEventContent>()?.let { content ->
content.sessionId ?: return@mapNotNull null
content.senderKey ?: return@mapNotNull null
SessionInfo(content.sessionId, content.senderKey)
}
}?.toSet()

View File

@ -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()

View File

@ -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() {

View File

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

View File

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

View File

@ -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<SwitchPreference>(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
}
}
}
/**

View File

@ -2958,6 +2958,9 @@
<string name="labs_enable_latex_maths">Enable LaTeX mathematics</string>
<string name="restart_the_application_to_apply_changes">Restart the application for the change to take effect.</string>
<string name="labs_enable_msc3061_share_history">MSC3061: Sharing room keys for past messages</string>
<string name="labs_enable_msc3061_share_history_desc">When inviting in an encrypted room that is sharing history, encrypted history will be visible.</string>
<!-- Poll -->
<string name="create_poll_title">Create Poll</string>
<string name="create_poll_question_title">Poll question or topic</string>

View File

@ -45,6 +45,13 @@
android:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
android:title="@string/labs_show_unread_notifications_as_tab" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:persistent="false"
android:key="SETTINGS_LABS_MSC3061_SHARE_KEYS_HISTORY"
android:summary="@string/labs_enable_msc3061_share_history_desc"
android:title="@string/labs_enable_msc3061_share_history" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_ENABLE_LATEX_MATHS"