Add lab flag and more tests

This commit is contained in:
Valere 2022-06-15 12:20:57 +02:00
parent d9fb58fbcb
commit 8e829c6aad
20 changed files with 490 additions and 61 deletions

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
@ -241,6 +248,37 @@ class CommonTestHelper internal 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 internal 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 internal 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 internal 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

@ -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)
@ -166,7 +163,7 @@ class E2eeSanityTests : InstrumentedTest {
delay(3_000) delay(3_000)
} }
// Due to the new shared keys implementation, invited user should be able to decrypt messages // check that messages are encrypted (uisi)
newAccount.forEach { otherSession -> newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
@ -174,7 +171,8 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
} }
timelineEvent != null && timelineEvent != null &&
timelineEvent.root.getClearType() == EventType.MESSAGE timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
} }
} }
} }
@ -739,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

@ -85,12 +85,16 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
val e2eRoomID = cryptoTestData.roomId val e2eRoomID = cryptoTestData.roomId
// Alice // Alice
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob // Bob
val bobSession = cryptoTestData.secondSession val bobSession = cryptoTestData.secondSession!!.also {
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!! it.cryptoService().enableShareKeyOnInvite(true)
}
val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
@ -114,7 +118,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
} }
// Create a new user // Create a new user
val arisSession = testHelper.createAccount("aris", SessionTestParams(true)) val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also {
it.cryptoService().enableShareKeyOnInvite(true)
}
Log.v("#E2E TEST", "Aris user created") Log.v("#E2E TEST", "Aris user created")
// Alice invites new user to the room // Alice invites new user to the room

View File

@ -39,9 +39,4 @@ data class MXCryptoConfig constructor(
*/ */
val limitRoomKeyRequestsToMyDevices: Boolean = false, val limitRoomKeyRequestsToMyDevices: Boolean = false,
/** )
* Flag that indicates whether or not key history will be shared to invited
* users with respect to room visibility
*/
val shouldShareKeyHistory: Boolean = true,
)

View File

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

View File

@ -110,7 +110,6 @@ import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.coroutineContext
import kotlin.math.max import kotlin.math.max
/** /**
@ -1118,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.

View File

@ -303,7 +303,7 @@ internal class MXMegolmDecryption(
* Returns boolean shared key flag, if enabled with respect to matrix configuration * Returns boolean shared key flag, if enabled with respect to matrix configuration
*/ */
private fun RoomKeyContent.getSharedKey(): Boolean { private fun RoomKeyContent.getSharedKey(): Boolean {
if (!matrixConfiguration.cryptoConfig.shouldShareKeyHistory) return false if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
return sharedHistory ?: false return sharedHistory ?: false
} }

View File

@ -1472,7 +1472,7 @@ internal class DefaultKeysBackupService @Inject constructor(
* Returns boolean shared key flag, if enabled with respect to matrix configuration * Returns boolean shared key flag, if enabled with respect to matrix configuration
*/ */
private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean { private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean {
if (!matrixConfiguration.cryptoConfig.shouldShareKeyHistory) return false if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
return sessionData.sharedHistory return sessionData.sharedHistory
} }

View File

@ -98,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.
* *

View File

@ -660,7 +660,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun shouldShareHistory(roomId: String): Boolean { override fun shouldShareHistory(roomId: String): Boolean {
if (!matrixConfiguration.cryptoConfig.shouldShareKeyHistory) return false if (!isShareKeysOnInviteEnabled()) return false
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory
} }
@ -1009,6 +1009,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

@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
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.crypto.store.db.migration.MigrateCryptoTo017
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
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
@ -52,7 +53,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 = 17L val schemaVersion = 18L
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")
@ -74,5 +75,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
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() if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
} }
} }

View File

@ -0,0 +1,36 @@
/*
* 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.internal.crypto.store.db.model.CryptoMetadataEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
/**
* Support for MSC3061 (share room keys for past messages as a flag)
*/
internal class MigrateCryptoTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) {
override fun doMigrate(realm: DynamicRealm) {
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)
}
}
}

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

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

@ -152,7 +152,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
} }
private suspend fun sendShareHistoryKeysIfNeeded(userId: String) { private suspend fun sendShareHistoryKeysIfNeeded(userId: String) {
if (!matrixConfiguration.cryptoConfig.shouldShareKeyHistory) return if (!cryptoService.isShareKeysOnInviteEnabled()) return
// TODO not sure it's the right way to get the latest messages in a room // TODO not sure it's the right way to get the latest messages in a room
val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use { val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use {
ChunkEntity.findLatestSessionInfo(it, roomId) ChunkEntity.findLatestSessionInfo(it, roomId)

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