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.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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.Session
import org.matrix.android.sdk.api.session.events.model.EventType 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.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.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.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline 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 org.matrix.android.sdk.api.session.sync.SyncState
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit
* This class exposes methods to be used in common cases * This class exposes methods to be used in common cases
* Registration, login, Sync, Sending messages... * Registration, login, Sync, Sending messages...
*/ */
class CommonTestHelper private constructor(context: Context) { class CommonTestHelper internal constructor(context: Context) {
companion object { companion object {
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) {
return sentEvents 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 * Reply in a thread
* @param room the room where to send the messages * @param room the room where to send the messages
@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) {
) )
assertNotNull(session) assertNotNull(session)
return session.also { return session.also {
// most of the test was created pre-MSC3061 so ensure compatibility
it.cryptoService().enableShareKeyOnInvite(false)
trackedSessions.add(session) trackedSessions.add(session)
} }
} }
@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) {
* @param latch * @param latch
* @throws InterruptedException * @throws InterruptedException
*/ */
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) {
assertTrue( assertTrue(
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.", "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)) { suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
while (true) { while (true) {
delay(1000) try {
delay(1000)
} catch (ex: CancellationException) {
// the job was canceled, just stop
return
}
if (condition()) { if (condition()) {
latch.countDown() latch.countDown()
return 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) { fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
coroutineScope.launch(dispatcher) { val job = coroutineScope.launch(dispatcher) {
block(latch) block(latch)
} }
await(latch, timeout) await(latch, timeout, job)
} }
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { 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.getRoom
import org.matrix.android.sdk.api.session.room.Room 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.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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/** /**
* @return alice session * @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 aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = testHelper.runBlockingTest { val roomId = testHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = roomHistoryVisibility
name = "MyRoom"
})
} }
if (encryptedRoom) { if (encryptedRoom) {
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/** /**
* @return alice and bob sessions * @return alice and bob sessions
*/ */
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId 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.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.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.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership 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.model.message.MessageContent
@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail import org.matrix.android.sdk.mustFail
import java.util.concurrent.CountDownLatch 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) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
class E2eeSanityTests : InstrumentedTest { class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) @get:Rule val rule = RetryTestRule(3)
@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest {
// All user should accept invite // All user should accept invite
otherAccounts.forEach { otherSession -> otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
} }
@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest {
} }
newAccount.forEach { newAccount.forEach {
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
} }
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, 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) { private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId -> 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) assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier() val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) 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.api.session.Session
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestData 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] * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
*/ */
internal data class KeysBackupScenarioData( internal data class KeysBackupScenarioData(
val cryptoTestData: CryptoTestData, val cryptoTestData: CryptoTestData,
val aliceKeys: List<OlmInboundGroupSessionWrapper2>, val aliceKeys: List<MXInboundMegolmSessionWrapper>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session val aliceSession2: Session
) { ) {

View File

@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest {
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg // - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session) val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
assertNotNull(keyBackupData) assertNotNull(keyBackupData)
assertNotNull(keyBackupData!!.sessionData) assertNotNull(keyBackupData!!.sessionData)
@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest {
val sessionData = keysBackup val sessionData = keysBackup
.decryptKeyBackupData( .decryptKeyBackupData(
keyBackupData, keyBackupData,
session.olmInboundGroupSession!!.sessionIdentifier(), session.safeSessionId!!,
cryptoTestData.roomId, cryptoTestData.roomId,
decryption!! decryption!!
) )

View File

@ -187,7 +187,7 @@ internal class KeysBackupTestHelper(
// - Alice must have the same keys on both devices // - Alice must have the same keys on both devices
for (aliceKey1 in testData.aliceKeys) { for (aliceKey1 in testData.aliceKeys) {
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
.getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!) .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
Assert.assertNotNull(aliceKey2) Assert.assertNotNull(aliceKey2)
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
} }

View File

@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest {
val roomName = "My Space" val roomName = "My Space"
val topic = "A public space for test" val topic = "A public space for test"
var spaceId: String = "" var spaceId: String = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(roomName, topic, null, true) 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.waitWithLatch {
commonTestHelper.retryPeriodicallyWithLatch(it) { 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 name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name)
assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic) assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set") // 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 androidx.lifecycle.Observer
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore import org.junit.Ignore
@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest {
val spaceName = "My Space" val spaceName = "My Space"
val topic = "A public space for test" val topic = "A public space for test"
var spaceId = "" var spaceId = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(spaceName, topic, null, true) spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
it.countDown()
} }
val syncedSpace = session.spaceService().getSpace(spaceId) val syncedSpace = session.spaceService().getSpace(spaceId)
var roomId = "" var roomId = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
it.countDown()
} }
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
syncedSpace!!.addChildren(roomId, viaServers, null, true) syncedSpace!!.addChildren(roomId, viaServers, null, true)
it.countDown()
} }
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
it.countDown()
} }
Thread.sleep(9000) commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
parents?.forEach {
parents?.forEach { Log.d("## TEST", "parent : $it")
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 // @Test
@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest {
// } // }
@Test @Test
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf( commonTestHelper,
Triple("A1", true /*auto-join*/, true/*canonical*/), session, "SpaceA",
Triple("A2", true, true) listOf(
) Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
) )
/* val spaceBInfo = */ createPublicSpace( /* val spaceBInfo = */ createPublicSpace(
session, "SpaceB", listOf( commonTestHelper,
Triple("B1", true /*auto-join*/, true/*canonical*/), session, "SpaceB",
Triple("B2", true, true), listOf(
Triple("B3", true, true) Triple("B1", true /*auto-join*/, true/*canonical*/),
) Triple("B2", true, true),
Triple("B3", true, true)
)
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf( commonTestHelper,
Triple("C1", true /*auto-join*/, true/*canonical*/), session, "SpaceC",
Triple("C2", true, true) listOf(
) Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
) )
// add C as a subspace of A // add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
it.countDown()
} }
// Create orphan rooms // Create orphan rooms
var orphan1 = "" var orphan1 = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
it.countDown()
} }
var orphan2 = "" var orphan2 = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
it.countDown()
} }
val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) 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" }) 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 // 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" }) val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
spaceA!!.addChildren(a3, viaServers, null, false) spaceA!!.addChildren(a3, viaServers, null, false)
it.countDown()
} }
Thread.sleep(6_000) Thread.sleep(6_000)
@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest {
@Test @Test
@Ignore("This test will be ignored until it is fixed") @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 session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf( commonTestHelper,
Triple("A1", true /*auto-join*/, true/*canonical*/), session, "SpaceA",
Triple("A2", true, true) listOf(
) Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf( commonTestHelper,
Triple("C1", true /*auto-join*/, true/*canonical*/), session, "SpaceC",
Triple("C2", true, true) listOf(
) Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
) )
// add C as a subspace of A // add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
it.countDown()
} }
// add back A as subspace of C // add back A as subspace of C
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
it.countDown()
} }
// A -> C -> A // A -> C -> A
@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest {
} }
@Test @Test
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper -> fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf( commonTestHelper,
Triple("A1", true /*auto-join*/, true/*canonical*/), session,
Triple("A2", true, true) "SpaceA",
) listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
) )
val spaceBInfo = createPublicSpace( val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf( commonTestHelper,
Triple("B1", true /*auto-join*/, true/*canonical*/), session,
Triple("B2", true, true), "SpaceB",
Triple("B3", true, true) listOf(
) Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
) )
// add B as a subspace of A // add B as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
runBlocking { commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
} }
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf( commonTestHelper,
Triple("C1", true /*auto-join*/, true/*canonical*/), session,
Triple("C2", true, true) "SpaceC",
) listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
) )
commonTestHelper.waitWithLatch { latch -> commonTestHelper.waitWithLatch { latch ->
@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest {
} }
} }
flatAChildren.observeForever(childObserver)
// add C as subspace of B // add C as subspace of B
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
// C1 and C2 should be in flatten child of A now // C1 and C2 should be in flatten child of A now
flatAChildren.observeForever(childObserver)
} }
// Test part one of the rooms // 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 // The room should have disapear from flat children
flatAChildren.observeForever(childObserver) flatAChildren.observeForever(childObserver)
// part from b room
session.roomService().leaveRoom(bRoomId)
} }
commonTestHelper.signOutAndClose(session) commonTestHelper.signOutAndClose(session)
} }
@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
private fun createPublicSpace( private fun createPublicSpace(
commonTestHelper: CommonTestHelper,
session: Session, session: Session,
spaceName: String, spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>> childInfo: List<Triple<String, Boolean, Boolean?>>
@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult { ): TestSpaceCreationResult {
var spaceId = "" var spaceId = ""
var roomIds: List<String> = emptyList() var roomIds: List<String> = emptyList()
runSessionTest(context()) { commonTestHelper -> commonTestHelper.runBlockingTest {
commonTestHelper.waitWithLatch { latch -> spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) val syncedSpace = session.spaceService().getSpace(spaceId)
val syncedSpace = session.spaceService().getSpace(spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds = childInfo.map { entry -> roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) 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) return TestSpaceCreationResult(spaceId, roomIds)
} }
private fun createPrivateSpace( private fun createPrivateSpace(
commonTestHelper: CommonTestHelper,
session: Session, session: Session,
spaceName: String, spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>> childInfo: List<Triple<String, Boolean, Boolean?>>
@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult { ): TestSpaceCreationResult {
var spaceId = "" var spaceId = ""
var roomIds: List<String> = emptyList() var roomIds: List<String> = emptyList()
runSessionTest(context()) { commonTestHelper -> commonTestHelper.runBlockingTest {
commonTestHelper.waitWithLatch { latch -> spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) val syncedSpace = session.spaceService().getSpace(spaceId)
val syncedSpace = session.spaceService().getSpace(spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") roomIds =
roomIds = childInfo.map { entry ->
childInfo.map { entry -> val homeServerCapabilities = session
val homeServerCapabilities = session .homeServerCapabilitiesService()
.homeServerCapabilitiesService() .getHomeServerCapabilities()
.getHomeServerCapabilities() session.roomService().createRoom(CreateRoomParams().apply {
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first
name = entry.first this.featurePreset = RestrictedRoomPreset(
this.featurePreset = RestrictedRoomPreset( homeServerCapabilities,
homeServerCapabilities, listOf(
listOf( RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
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)
} }
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) return TestSpaceCreationResult(spaceId, roomIds)
@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest {
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
/* val spaceAInfo = */ createPublicSpace( /* val spaceAInfo = */ createPublicSpace(
session, "SpaceA", listOf( commonTestHelper,
Triple("A1", true /*auto-join*/, true/*canonical*/), session, "SpaceA",
Triple("A2", true, true) listOf(
) Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
) )
val spaceBInfo = createPublicSpace( val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf( commonTestHelper,
Triple("B1", true /*auto-join*/, true/*canonical*/), session, "SpaceB",
Triple("B2", true, true), listOf(
Triple("B3", true, true) Triple("B1", true /*auto-join*/, true/*canonical*/),
) Triple("B2", true, true),
Triple("B3", true, true)
)
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf( commonTestHelper,
Triple("C1", true /*auto-join*/, true/*canonical*/), session, "SpaceC",
Triple("C2", true, true) listOf(
) Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
) )
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest {
runBlocking { runBlocking {
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
Thread.sleep(6_000)
} }
// Thread.sleep(4_000) // Thread.sleep(4_000)
@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest {
// + C // + C
// + c1, c2 // + c1, c2
val rootSpaces = commonTestHelper.runBlockingTest { commonTestHelper.waitWithLatch { latch ->
session.spaceService().getRootSpaceSummaries() 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 @Test
@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest {
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
val spaceAInfo = createPrivateSpace( val spaceAInfo = createPrivateSpace(
aliceSession, "Private Space A", listOf( commonTestHelper,
Triple("General", true /*suggested*/, true/*canonical*/), aliceSession, "Private Space A",
Triple("Random", true, true) listOf(
) Triple("General", true /*suggested*/, true/*canonical*/),
Triple("Random", true, true)
)
) )
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest {
} }
var bobRoomId = "" var bobRoomId = ""
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
it.countDown()
} }
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest {
} }
} }
commonTestHelper.waitWithLatch { commonTestHelper.runBlockingTest {
bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
it.countDown()
} }
commonTestHelper.waitWithLatch { latch -> 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` * You can limit request only to your sessions by turning this setting to `true`
*/ */
val limitRoomKeyRequestsToMyDevices: Boolean = false, 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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
interface CryptoService { interface CryptoService {
@ -84,6 +85,20 @@ interface CryptoService {
fun isKeyGossipingEnabled(): Boolean 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 setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int fun getDeviceTrackingStatus(userId: String): Int
@ -176,4 +191,9 @@ interface CryptoService {
* send, in order to speed up sending of the message. * send, in order to speed up sending of the message.
*/ */
fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) 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. * private part of this key unless they have done device verification.
*/ */
@Json(name = "sender_claimed_ed25519_key") @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 // should be a Long but it is sometimes a double
@Json(name = "chain_index") @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 @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.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent 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.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.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction 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.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService 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.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.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore 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) { private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) return if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>() val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
eventContent?.historyVisibility?.let { val historyVisibility = eventContent?.historyVisibility
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) 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 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. * Tells whether the client should ever send encrypted messages to unverified devices.
* The default value is false. * 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 * 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.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag 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 org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber import timber.log.Timber
import java.util.Timer import java.util.Timer
@ -31,7 +31,7 @@ import java.util.TimerTask
import javax.inject.Inject import javax.inject.Inject
internal data class InboundGroupSessionHolder( internal data class InboundGroupSessionHolder(
val wrapper: OlmInboundGroupSessionWrapper2, val wrapper: MXInboundMegolmSessionWrapper,
val mutex: Mutex = Mutex() val mutex: Mutex = Mutex()
) )
@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) 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 val timer = Timer()
private var timerTask: TimerTask? = null private var timerTask: TimerTask? = null
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>() private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
@Synchronized @Synchronized
fun clear() { fun clear() {
@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor(
@Synchronized @Synchronized
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") 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) store.removeInboundGroupSession(sessionId, senderKey)
sessionCache.remove(CacheKey(sessionId, senderKey)) sessionCache.remove(CacheKey(sessionId, senderKey))
// release removed session // release removed session
old.wrapper.olmInboundGroupSession?.releaseSession() old.wrapper.session.releaseSession()
internalStoreGroupSession(new, sessionId, senderKey) internalStoreGroupSession(new, sessionId, senderKey)
} }
@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor(
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { 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}") 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 // We want to batch this a bit for performances
dirtySession.add(holder.wrapper) dirtySession.add(holder)
if (sessionCache[CacheKey(sessionId, senderKey)] == null) { if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert // first time seen, put it in memory cache while waiting for batch insert
@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor(
@Synchronized @Synchronized
private fun batchSave() { private fun batchSave() {
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) } val toSave = mutableListOf<InboundGroupSessionHolder>().apply { addAll(dirtySession) }
dirtySession.clear() dirtySession.clear()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}") Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
tryOrNull { 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.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo 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.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.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.MoshiProvider 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.android.sdk.internal.util.time.Clock
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import org.matrix.olm.OlmInboundGroupSession
import org.matrix.olm.OlmMessage import org.matrix.olm.OlmMessage
import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor(
return MXOutboundSessionInfo( return MXOutboundSessionInfo(
sessionId = sessionId, sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, store), sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
clock, clock = clock,
restoredOutboundGroupSession.creationTime creationTime = restoredOutboundGroupSession.creationTime,
sharedHistory = restoredOutboundGroupSession.sharedHistory
) )
} }
return null return null
@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor(
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
* @param keysClaimed Other keys the sender claims. * @param keysClaimed Other keys the sender claims.
* @param exportFormat true if the megolm keys are in export format * @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. * @return true if the operation succeeds.
*/ */
fun addInboundGroupSession( fun addInboundGroupSession(sessionId: String,
sessionId: String, sessionKey: String,
sessionKey: String, roomId: String,
roomId: String, senderKey: String,
senderKey: String, forwardingCurve25519KeyChain: List<String>,
forwardingCurve25519KeyChain: List<String>, keysClaimed: Map<String, String>,
keysClaimed: Map<String, String>, exportFormat: Boolean,
exportFormat: Boolean sharedHistory: Boolean): AddSessionResult {
): AddSessionResult { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) if (exportFormat) {
OlmInboundGroupSession.importSession(sessionKey)
} else {
OlmInboundGroupSession(sessionKey)
}
}
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper val existingSession = existingSessionHolder?.wrapper
// If we have an existing one we should check if the new one is not better // If we have an existing one we should check if the new one is not better
if (existingSession != null) { if (existingSession != null) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
try { 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? // This is quite unexpected, could throw if native was released?
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession?.releaseSession()
// Probably should discard it? // 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 our existing session is better we keep it
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession?.releaseSession()
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession?.releaseSession()
return AddSessionResult.NotImported 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") Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
// sanity check on the new session // sanity check on the new session
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession if (null == candidateSession) {
if (null == candidateOlmInboundSession) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>") Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
return AddSessionResult.NotImported return AddSessionResult.NotImported
} }
try { try {
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { if (candidateSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
candidateOlmInboundSession.releaseSession() candidateSession.releaseSession()
return AddSessionResult.NotImported return AddSessionResult.NotImported
} }
} catch (e: Throwable) { } catch (e: Throwable) {
candidateOlmInboundSession.releaseSession() candidateSession.releaseSession()
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
return AddSessionResult.NotImported return AddSessionResult.NotImported
} }
candidateSession.senderKey = senderKey val candidateSessionData = InboundGroupSessionData(
candidateSession.roomId = roomId senderKey = senderKey,
candidateSession.keysClaimed = keysClaimed roomId = roomId,
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain keysClaimed = keysClaimed,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
sharedHistory = sharedHistory,
)
val wrapper = MXInboundMegolmSessionWrapper(
candidateSession,
candidateSessionData
)
if (existingSession != null) { if (existingSession != null) {
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey)
} else { } 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 * @param megolmSessionsData the megolm sessions data
* @return the successfully imported sessions. * @return the successfully imported sessions.
*/ */
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> { fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size) val sessions = ArrayList<MXInboundMegolmSessionWrapper>(megolmSessionsData.size)
for (megolmSessionData in megolmSessionsData) { for (megolmSessionData in megolmSessionsData) {
val sessionId = megolmSessionData.sessionId ?: continue val sessionId = megolmSessionData.sessionId ?: continue
val senderKey = megolmSessionData.senderKey ?: continue val senderKey = megolmSessionData.senderKey ?: continue
val roomId = megolmSessionData.roomId val roomId = megolmSessionData.roomId
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null val candidateSessionToImport = try {
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
try { } catch (e: Throwable) {
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
} 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()
continue continue
} }
val candidateOlmInboundGroupSession = candidateSessionToImport.session
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper val existingSession = existingSessionHolder?.wrapper
@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor(
sessions.add(candidateSessionToImport) sessions.add(candidateSessionToImport)
} else { } else {
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex }
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex }
if (existingFirstKnown == null || candidateFirstKnownIndex == null) { if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
// should not happen? // should not happen?
candidateSessionToImport.olmInboundGroupSession?.releaseSession() candidateSessionToImport.session.releaseSession()
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
} else { } else {
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { if (existingFirstKnown <= candidateFirstKnownIndex) {
// Ignore this, keep existing // Ignore this, keep existing
candidateOlmInboundGroupSession.releaseSession() candidateOlmInboundGroupSession.releaseSession()
} else { } else {
@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor(
): OlmDecryptionResult { ): OlmDecryptionResult {
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
val wrapper = sessionHolder.wrapper val wrapper = sessionHolder.wrapper
val inboundGroupSession = wrapper.olmInboundGroupSession val inboundGroupSession = wrapper.session
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
if (roomId != wrapper.roomId) { if (roomId != wrapper.roomId) {
// Check that the room id matches the original one for the session. This stops // Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room. // the HS pretending a message was targeting a different room.
@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor(
return OlmDecryptionResult( return OlmDecryptionResult(
payload, payload,
wrapper.keysClaimed, wrapper.sessionData.keysClaimed,
senderKey, 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). * Devices which forwarded this session to us (normally empty).
*/ */
@Json(name = "forwarding_curve25519_key_chain") @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)) { if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
// let's see what's the index // let's see what's the index
val knownIndex = tryOrNull { 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) { 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 // 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 ?: "", megolmSessionData.senderKey ?: "",
tryOrNull { tryOrNull {
olmInboundGroupSessionWrappers olmInboundGroupSessionWrappers
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
?.firstKnownIndex?.toInt() ?.session?.firstKnownIndex
?.toInt()
} ?: 0 } ?: 0
) )

View File

@ -16,7 +16,9 @@
package org.matrix.android.sdk.internal.crypto.algorithms 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.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
/** /**
* An interface for encrypting data. * An interface for encrypting data.
@ -32,4 +34,6 @@ internal interface IMXEncrypting {
* @return the encrypted content * @return the encrypted content
*/ */
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): 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 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.logger.LoggerTag 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.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.NewSessionListener
@ -41,6 +42,7 @@ internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration,
private val liveEventManager: Lazy<StreamEventsManager> private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting { ) : IMXDecrypting {
@ -240,13 +242,14 @@ internal class MXMegolmDecryption(
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val addSessionResult = olmDevice.addInboundGroupSession( val addSessionResult = olmDevice.addInboundGroupSession(
roomKeyContent.sessionId, sessionId = roomKeyContent.sessionId,
roomKeyContent.sessionKey, sessionKey = roomKeyContent.sessionKey,
roomKeyContent.roomId, roomId = roomKeyContent.roomId,
senderKey, senderKey = senderKey,
forwardingCurve25519KeyChain, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
keysClaimed, keysClaimed = keysClaimed,
exportFormat exportFormat = exportFormat,
sharedHistory = roomKeyContent.getSharedKey()
) )
when (addSessionResult) { 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. * 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 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy 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.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration,
private val eventsManager: Lazy<StreamEventsManager> private val eventsManager: Lazy<StreamEventsManager>
) { ) {
@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
olmDevice, olmDevice,
outgoingKeyRequestManager, outgoingKeyRequestManager,
cryptoStore, 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.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode 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.DeviceListManager
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
import org.matrix.android.sdk.internal.crypto.MXOlmDevice 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.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@ -151,14 +152,27 @@ internal class MXMegolmEncryption(
"ed25519" to olmDevice.deviceEd25519Key!! "ed25519" to olmDevice.deviceEd25519Key!!
) )
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
olmDevice.addInboundGroupSession( olmDevice.addInboundGroupSession(
sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!, sessionId = sessionId!!,
emptyList(), keysClaimedMap, false sessionKey = olmDevice.getSessionKey(sessionId)!!,
roomId = roomId,
senderKey = olmDevice.deviceCurve25519Key!!,
forwardingCurve25519KeyChain = emptyList(),
keysClaimed = keysClaimedMap,
exportFormat = false,
sharedHistory = sharedHistory
) )
defaultKeysBackupService.maybeBackupKeys() 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 || if (session == null ||
// Need to make a brand new session? // Need to make a brand new session?
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || 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 // Determine if we have shared with anyone we shouldn't have
session.sharedWithTooManyDevices(devicesInRoom)) { session.sharedWithTooManyDevices(devicesInRoom)) {
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") 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. * Share the device keys of a an user.
* *
* @param session the session info * @param sessionInfo the session info
* @param devicesByUser the devices map * @param devicesByUser the devices map
*/ */
private suspend fun shareUserDevicesKey( private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
session: MXOutboundSessionInfo, devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
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 sessionKey = olmDevice.getSessionKey(session.sessionId) }
val chainIndex = olmDevice.getMessageIndex(session.sessionId) val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
val submap = HashMap<String, Any>() val payload = mapOf(
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM "type" to EventType.ROOM_KEY,
submap["room_id"] = roomId "content" to mapOf(
submap["session_id"] = session.sessionId "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
submap["session_key"] = sessionKey!! "room_id" to roomId,
submap["chain_index"] = chainIndex "session_id" to sessionInfo.sessionId,
"session_key" to sessionKey,
val payload = HashMap<String, Any>() "chain_index" to chainIndex,
payload["type"] = EventType.ROOM_KEY "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
payload["content"] = submap )
)
var t0 = clock.epochMillis() var t0 = clock.epochMillis()
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
@ -292,7 +309,7 @@ internal class MXMegolmEncryption(
// for dead devices on every message. // for dead devices on every message.
for ((_, devicesToShareWith) in devicesByUser) { for ((_, devicesToShareWith) in devicesByUser) {
for (deviceInfo in devicesToShareWith) { 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? // XXX is it needed to add it to the audit trail?
// For now decided that no, we are more interested by forward trail // For now decided that no, we are more interested by forward trail
} }
@ -300,8 +317,8 @@ internal class MXMegolmEncryption(
if (haveTargets) { if (haveTargets) {
t0 = clock.epochMillis() t0 = clock.epochMillis()
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
try { try {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
@ -310,7 +327,7 @@ internal class MXMegolmEncryption(
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
} catch (failure: Throwable) { } catch (failure: Throwable) {
// What to do here... // 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 { } else {
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") 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? // XXX offload?, as they won't read the message anyhow?
notifyKeyWithHeld( notifyKeyWithHeld(
noOlmToNotify, noOlmToNotify,
session.sessionId, sessionInfo.sessionId,
olmDevice.deviceCurve25519Key, olmDevice.deviceCurve25519Key,
WithHeldCode.NO_OLM 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( data class DeviceInRoomInfo(
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(), val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap() val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()

View File

@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo(
private val clock: Clock, private val clock: Clock,
// When the session was created // When the session was created
private val creationTime: Long = clock.epochMillis(), private val creationTime: Long = clock.epochMillis(),
val sharedHistory: Boolean = false
) { ) {
// Number of times this session has been used // 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.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback 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.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP 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.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.fromBase64 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.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.ObjectSigner 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.GetSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask 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.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.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor(
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor // Task executor
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val matrixConfiguration: MatrixConfiguration,
private val inboundGroupSessionStore: InboundGroupSessionStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
) : KeysBackupService { ) : KeysBackupService {
@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor(
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
try { try {
encryptGroupSession(olmInboundGroupSessionWrapper) encryptGroupSession(olmInboundGroupSessionWrapper)
@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor(
@VisibleForTesting @VisibleForTesting
@WorkerThread @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 // 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 // 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 // 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( val sessionBackupData = mapOf(
"algorithm" to sessionData.algorithm, "algorithm" to sessionData.algorithm,
"sender_key" to sessionData.senderKey, "sender_key" to sessionData.senderKey,
"sender_claimed_keys" to sessionData.senderClaimedKeys, "sender_claimed_keys" to sessionData.senderClaimedKeys,
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), "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() val json = MoshiProvider.providesMoshi()
@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor(
.toJson(sessionBackupData) .toJson(sessionBackupData)
val encryptedSessionBackupData = try { val encryptedSessionBackupData = try {
backupOlmPkEncryption?.encrypt(json) withContext(coroutineDispatchers.computation) {
backupOlmPkEncryption?.encrypt(json)
}
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
null null
@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor(
// Build backup data for that key // Build backup data for that key
return KeyBackupData( return KeyBackupData(
firstMessageIndex = try { firstMessageIndex = try {
olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0 olmInboundGroupSessionWrapper.session.firstKnownIndex
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
0L 0L
}, },
forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size, forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
isVerified = device?.isVerified == true, isVerified = device?.isVerified == true,
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
sessionData = mapOf( sessionData = mapOf(
"ciphertext" to encryptedSessionBackupData.mCipherText, "ciphertext" to encryptedSessionBackupData.mCipherText,
"mac" to encryptedSessionBackupData.mMac, "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 @VisibleForTesting
@WorkerThread @WorkerThread
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {

View File

@ -50,5 +50,12 @@ internal data class KeyBackupData(
* Algorithm-dependent data. * Algorithm-dependent data.
*/ */
@Json(name = "session_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 class adds more context to a OlmInboundGroupSession object.
* This allows additional checks. The class implements Serializable so that the context can be stored. * 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 { internal class OlmInboundGroupSessionWrapper2 : Serializable {
// The associated olm inbound group session. // The associated olm inbound group session.

View File

@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession
internal data class OutboundGroupSessionWrapper( internal data class OutboundGroupSessionWrapper(
val outboundGroupSession: OlmOutboundGroupSession, 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.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode 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.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.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity 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. * @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. * @return true to unilaterally blacklist all unverified devices.
@ -90,6 +98,20 @@ internal interface IMXCryptoStore {
fun isKeyGossipingEnabled(): Boolean 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. * 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 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. * 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. * @param sessions the inbound group sessions to store.
*/ */
fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
/** /**
* Retrieve an inbound group session. * Retrieve an inbound group session.
@ -299,7 +332,17 @@ internal interface IMXCryptoStore {
* @param senderKey the base64-encoded curve25519 key of the sender. * @param senderKey the base64-encoded curve25519 key of the sender.
* @return an inbound group session. * @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. * Get the current outbound group session for this encrypted room.
@ -333,7 +376,7 @@ internal interface IMXCryptoStore {
* *
* @param olmInboundGroupSessionWrappers the sessions * @param olmInboundGroupSessionWrappers the sessions
*/ */
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
/** /**
* Retrieve inbound group sessions that are not yet backed up. * 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. * @param limit the maximum number of sessions to return.
* @return an array of non backed up inbound group sessions. * @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. * 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.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional 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.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor(
?: false ?: 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) { override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers 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) { override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
var sessionIdentifier: String? = null 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()) { if (sessions.isEmpty()) {
return return
} }
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
sessions.forEach { session -> sessions.forEach { wrapper ->
var sessionIdentifier: String? = null
try { val sessionIdentifier = try {
sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier() wrapper.session.sessionIdentifier()
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
return@forEach
} }
if (sessionIdentifier != null) { // val shouldShareHistory = session.roomId?.let { roomId ->
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) // CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory
// } ?: false
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
val existing = realm.where<OlmInboundGroupSessionEntity>() val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) primaryKey = key
.findFirst() store(wrapper)
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)
}
} }
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) val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) { realm ->
it.where<OlmInboundGroupSessionEntity>() realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst() .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 { entity.getOutboundGroupSession()?.let {
OutboundGroupSessionWrapper( OutboundGroupSessionWrapper(
it, it,
entity.creationTime ?: 0 entity.creationTime ?: 0,
entity.shouldShareHistory
) )
} }
} }
@ -806,6 +824,8 @@ internal class RealmCryptoStore @Inject constructor(
if (outboundGroupSession != null) { if (outboundGroupSession != null) {
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
creationTime = clock.epochMillis() creationTime = clock.epochMillis()
// Store the room history visibility on the outbound session creation
shouldShareHistory = entity.shouldShareHistory
putOutboundGroupSession(outboundGroupSession) putOutboundGroupSession(outboundGroupSession)
} }
entity.outboundSessionInfo = info 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, * 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. * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management.
*/ */
override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> { override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) { realm ->
it.where<OlmInboundGroupSessionEntity>() realm.where<OlmInboundGroupSessionEntity>()
.findAll() .findAll()
.mapNotNull { inboundGroupSessionEntity -> .mapNotNull { it.toModel() }
inboundGroupSessionEntity.getInboundGroupSession() }
} }
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()) { if (olmInboundGroupSessionWrappers.isEmpty()) {
return return
} }
@ -893,10 +928,13 @@ internal class RealmCryptoStore @Inject constructor(
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try { try {
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() val sessionIdentifier =
tryOrNull("Failed to get session identifier") {
olmInboundGroupSessionWrapper.session.sessionIdentifier()
} ?: return@forEach
val key = OlmInboundGroupSessionEntity.createPrimaryKey( val key = OlmInboundGroupSessionEntity.createPrimaryKey(
sessionIdentifier, sessionIdentifier,
olmInboundGroupSessionWrapper.senderKey olmInboundGroupSessionWrapper.sessionData.senderKey
) )
val existing = realm.where<OlmInboundGroupSessionEntity>() 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 // ... might be in cache but not yet persisted, create a record to persist backedup state
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key primaryKey = key
sessionId = sessionIdentifier store(olmInboundGroupSessionWrapper)
senderKey = olmInboundGroupSessionWrapper.senderKey
putInboundGroupSession(olmInboundGroupSessionWrapper)
backedUp = true 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) { return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>() it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
.limit(limit.toLong()) .limit(limit.toLong())
.findAll() .findAll()
.mapNotNull { inboundGroupSession -> .mapNotNull { it.toModel() }
inboundGroupSession.getInboundGroupSession()
}
} }
} }
@ -973,6 +1007,18 @@ internal class RealmCryptoStore @Inject constructor(
} ?: false } ?: 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) { override fun setDeviceKeysUploaded(uploaded: Boolean) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded 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.MigrateCryptoTo014
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 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.MigrateCryptoTo016
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -51,7 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
// 0, 1, 2: legacy Riot-Android // 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema // 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) // 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) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") 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 < 14) MigrateCryptoTo014(realm).perform()
if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
if (oldVersion < 16) MigrateCryptoTo016(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, var globalBlacklistUnverifiedDevices: Boolean = false,
// setting to enable or disable key gossiping // setting to enable or disable key gossiping
var globalEnableKeyGossiping: Boolean = true, 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. // The keys backup version currently used. Null means no backup.
var backupVersion: String? = null, var backupVersion: String? = null,

View File

@ -24,6 +24,8 @@ internal open class CryptoRoomEntity(
var algorithm: String? = null, var algorithm: String? = null,
var shouldEncryptForInvitedMembers: Boolean? = null, var shouldEncryptForInvitedMembers: Boolean? = null,
var blacklistUnverifiedDevices: Boolean = false, 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, // Store the current outbound session for this room,
// to avoid re-create and re-share at each startup (if rotation not needed..) // 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 // 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.RealmObject
import io.realm.annotations.PrimaryKey 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.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm 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 import timber.log.Timber
internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" 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( internal open class OlmInboundGroupSessionEntity(
// Combined value to build a primary key // Combined value to build a primary key
@PrimaryKey var primaryKey: String? = null, @PrimaryKey var primaryKey: String? = null,
// denormalization for faster querying (these fields are in the inboundGroupSessionDataJson)
var sessionId: String? = null, var sessionId: String? = null,
var senderKey: 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, 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 // Indicate if the key has been backed up to the homeserver
var backedUp: Boolean = false var backedUp: Boolean = false
) : ) :
RealmObject() { 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 { return try {
deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData) deserializeFromRealm(serializedOlmInboundGroupSession)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## Deserialization failure") Timber.e(failure, "## Deserialization failure")
return null return null
} }
} }
fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { fun getData(): InboundGroupSessionData? {
olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) 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( internal open class OutboundGroupSessionInfoEntity(
var serializedOutboundSessionData: String? = null, var serializedOutboundSessionData: String? = null,
var creationTime: Long? = null var creationTime: Long? = null,
var shouldShareHistory: Boolean = false
) : RealmObject() { ) : RealmObject() {
fun getOutboundGroupSession(): OlmOutboundGroupSession? { fun getOutboundGroupSession(): OlmOutboundGroupSession? {

View File

@ -15,7 +15,6 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks 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.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor(
params.event.roomId params.event.roomId
?.takeIf { params.encrypt } ?.takeIf { params.encrypt }
?.let { roomId -> ?.let { roomId ->
tryOrNull { try {
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) 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.Realm
import io.realm.kotlin.createObject 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.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.ChunkEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity 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.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields 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.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.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection 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 // We don't know, so we assume it's false
return 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.Module
import dagger.Provides import dagger.Provides
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
import okhttp3.Dispatcher
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@ -73,7 +74,9 @@ internal object NetworkModule {
apiInterceptor: ApiInterceptor apiInterceptor: ApiInterceptor
): OkHttpClient { ): OkHttpClient {
val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build() val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
val dispatcher = Dispatcher().apply {
maxRequestsPerHost = 20
}
return OkHttpClient.Builder() return OkHttpClient.Builder()
// workaround for #4669 // workaround for #4669
.protocols(listOf(Protocol.HTTP_1_1)) .protocols(listOf(Protocol.HTTP_1_1))
@ -94,6 +97,7 @@ internal object NetworkModule {
addInterceptor(curlLoggingInterceptor) addInterceptor(curlLoggingInterceptor)
} }
} }
.dispatcher(dispatcher)
.connectionSpecs(Collections.singletonList(spec)) .connectionSpecs(Collections.singletonList(spec))
.applyMatrixConfiguration(matrixConfiguration) .applyMatrixConfiguration(matrixConfiguration)
.build() .build()

View File

@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con
override fun register(hasChanged: () -> Unit) { override fun register(hasChanged: () -> Unit) {
hasChangedCallback = hasChanged 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() { override fun unregister() {

View File

@ -17,18 +17,23 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.otaliastudios.opengl.core.use
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery 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.identity.ThreePid
import org.matrix.android.sdk.api.session.room.members.MembershipService 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.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership 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.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.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.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
@ -50,8 +55,10 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val inviteThreePidTask: InviteThreePidTask, private val inviteThreePidTask: InviteThreePidTask,
private val membershipAdminTask: MembershipAdminTask, private val membershipAdminTask: MembershipAdminTask,
private val roomDataSource: RoomDataSource, private val roomDataSource: RoomDataSource,
private val cryptoService: CryptoService,
@UserId @UserId
private val userId: String, private val userId: String,
private val matrixConfiguration: MatrixConfiguration,
private val queryStringValueProcessor: QueryStringValueProcessor private val queryStringValueProcessor: QueryStringValueProcessor
) : MembershipService { ) : MembershipService {
@ -139,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor(
} }
override suspend fun invite(userId: String, reason: String?) { override suspend fun invite(userId: String, reason: String?) {
sendShareHistoryKeysIfNeeded(userId)
val params = InviteTask.Params(roomId, userId, reason) val params = InviteTask.Params(roomId, userId, reason)
inviteTask.execute(params) 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) { override suspend fun invite3pid(threePid: ThreePid) {
val params = InviteThreePidTask.Params(roomId, threePid) val params = InviteThreePidTask.Params(roomId, threePid)
return inviteThreePidTask.execute(params) 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_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" 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 // 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" 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" 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.text.method.LinkMovementMethod
import android.widget.TextView import android.widget.TextView
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreference
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
@ -57,6 +58,17 @@ class VectorSettingsLabsFragment @Inject constructor(
false 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="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="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 --> <!-- Poll -->
<string name="create_poll_title">Create Poll</string> <string name="create_poll_title">Create Poll</string>
<string name="create_poll_question_title">Poll question or topic</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:key="SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
android:title="@string/labs_show_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 <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="SETTINGS_LABS_ENABLE_LATEX_MATHS" android:key="SETTINGS_LABS_ENABLE_LATEX_MATHS"