mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Add integration tests for shared keys rotation on room history visibility change
This commit is contained in:
parent
3a5b737639
commit
2e88998b05
2 changed files with 375 additions and 229 deletions
|
@ -0,0 +1,375 @@
|
||||||
|
/*
|
||||||
|
* 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.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.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
|
||||||
|
|
||||||
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
|
// Alice
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
|
// Bob
|
||||||
|
val bobSession = cryptoTestData.secondSession
|
||||||
|
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)?.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))
|
||||||
|
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.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)?.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)?.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
|
||||||
|
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
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation has already been done so we do not need to rotate again
|
||||||
|
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), false)
|
||||||
|
Log.v("#E2E TEST ROTATION", "No rotation needed yet")
|
||||||
|
|
||||||
|
// Let's change the room history visibility
|
||||||
|
testHelper.waitWithLatch {
|
||||||
|
aliceRoomPOV.sendStateEvent(
|
||||||
|
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
stateKey = "",
|
||||||
|
body = RoomHistoryVisibilityContent(_historyVisibility = nextRoomHistoryVisibility._historyVisibility).toContent()
|
||||||
|
)
|
||||||
|
it.countDown()
|
||||||
|
}
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)
|
||||||
|
?.content
|
||||||
|
?.toModel<RoomHistoryVisibilityContent>()
|
||||||
|
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
|
||||||
|
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
|
||||||
|
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), false)
|
||||||
|
Log.v("#E2E TEST ROTATION", "Rotation is not needed")
|
||||||
|
}
|
||||||
|
initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> {
|
||||||
|
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), true)
|
||||||
|
Log.v("#E2E TEST ROTATION", "Rotation is needed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(testHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
|
||||||
|
aliceRoomPOV.sendTextMessage(text)
|
||||||
|
var sentEventId: String? = null
|
||||||
|
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||||
|
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
|
||||||
|
timeline.start()
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val decryptedMsg = timeline.getSnapshot()
|
||||||
|
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||||
|
.also { list ->
|
||||||
|
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
||||||
|
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||||
|
}
|
||||||
|
.filter { it.root.sendState == SendState.SYNCED }
|
||||||
|
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
||||||
|
sentEventId = decryptedMsg?.eventId
|
||||||
|
decryptedMsg != null
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.dispose()
|
||||||
|
}
|
||||||
|
return sentEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,229 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
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.message.MessageContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
|
||||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
|
||||||
|
|
||||||
@RunWith(JUnit4::class)
|
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
|
||||||
@LargeTest
|
|
||||||
class E2eeShareKeysSanityTest : InstrumentedTest {
|
|
||||||
|
|
||||||
private val testHelper = CommonTestHelper(context())
|
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShareMessagesHistoryWithRoomWorldReadable() {
|
|
||||||
testSharedHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShareMessagesHistoryWithRoomShared() {
|
|
||||||
testSharedHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShareMessagesHistoryWithRoomJoined() {
|
|
||||||
testSharedHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShareMessagesHistoryWithRoomInvited() {
|
|
||||||
testSharedHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple test that creates an e2ee room.
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
private fun testSharedHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) {
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
|
|
||||||
|
|
||||||
val e2eRoomID = cryptoTestData.roomId
|
|
||||||
|
|
||||||
// Alice
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
|
||||||
|
|
||||||
// Bob
|
|
||||||
val bobSession = cryptoTestData.secondSession
|
|
||||||
val bobRoomPOV = bobSession!!.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!")
|
|
||||||
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.getRoom(e2eRoomID)?.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))
|
|
||||||
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.invite(arisSession.myUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID)
|
|
||||||
|
|
||||||
ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID)
|
|
||||||
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.getRoom(e2eRoomID)?.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.getRoom(e2eRoomID)?.getTimelineEvent(aliceMessageId!!)
|
|
||||||
timelineEvent == null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.signOutAndClose(arisSession)
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
|
||||||
aliceRoomPOV.sendTextMessage(text)
|
|
||||||
var sentEventId: String? = null
|
|
||||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
|
||||||
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
|
|
||||||
timeline.start()
|
|
||||||
|
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
val decryptedMsg = timeline.getSnapshot()
|
|
||||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
|
||||||
.also { list ->
|
|
||||||
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
|
||||||
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
|
||||||
}
|
|
||||||
.filter { it.root.sendState == SendState.SYNCED }
|
|
||||||
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
|
||||||
sentEventId = decryptedMsg?.eventId
|
|
||||||
decryptedMsg != null
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline.dispose()
|
|
||||||
}
|
|
||||||
return sentEventId
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
|
||||||
testHelper.waitWithLatch { latch ->
|
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
otherAccounts.map {
|
|
||||||
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
|
|
||||||
}.all {
|
|
||||||
it == Membership.JOIN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun waitForAndAcceptInviteInRoom(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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testHelper.runBlockingTest(60_000) {
|
|
||||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
|
||||||
try {
|
|
||||||
otherSession.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue