Skip to content

Commit 607c959

Browse files
ariskotsomitopoulosBillCarsonFr
authored andcommitted
Add integration tests for shared keys rotation on room history visibility change
1 parent 055e8e8 commit 607c959

File tree

2 files changed

+375
-229
lines changed

2 files changed

+375
-229
lines changed
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.crypto
18+
19+
import android.util.Log
20+
import androidx.test.filters.LargeTest
21+
import org.amshove.kluent.internal.assertEquals
22+
import org.junit.Assert
23+
import org.junit.FixMethodOrder
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
import org.junit.runners.JUnit4
27+
import org.junit.runners.MethodSorters
28+
import org.matrix.android.sdk.InstrumentedTest
29+
import org.matrix.android.sdk.api.session.Session
30+
import org.matrix.android.sdk.api.session.events.model.EventType
31+
import org.matrix.android.sdk.api.session.events.model.toContent
32+
import org.matrix.android.sdk.api.session.events.model.toModel
33+
import org.matrix.android.sdk.api.session.getRoom
34+
import org.matrix.android.sdk.api.session.room.Room
35+
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
36+
import org.matrix.android.sdk.api.session.room.model.Membership
37+
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
38+
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
39+
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
40+
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
41+
import org.matrix.android.sdk.api.session.room.send.SendState
42+
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
43+
import org.matrix.android.sdk.common.CommonTestHelper
44+
import org.matrix.android.sdk.common.CryptoTestHelper
45+
import org.matrix.android.sdk.common.SessionTestParams
46+
import org.matrix.android.sdk.common.TestConstants
47+
48+
@RunWith(JUnit4::class)
49+
@FixMethodOrder(MethodSorters.JVM)
50+
@LargeTest
51+
class E2eeShareKeysHistoryTest : InstrumentedTest {
52+
53+
@Test
54+
fun testShareMessagesHistoryWithRoomWorldReadable() {
55+
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE)
56+
}
57+
58+
@Test
59+
fun testShareMessagesHistoryWithRoomShared() {
60+
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED)
61+
}
62+
63+
@Test
64+
fun testShareMessagesHistoryWithRoomJoined() {
65+
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED)
66+
}
67+
68+
@Test
69+
fun testShareMessagesHistoryWithRoomInvited() {
70+
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED)
71+
}
72+
73+
/**
74+
* In this test we create a room and test that new members
75+
* can decrypt history when the room visibility is
76+
* RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE.
77+
* We should not be able to view messages/decrypt otherwise
78+
*/
79+
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) {
80+
val testHelper = CommonTestHelper(context())
81+
val cryptoTestHelper = CryptoTestHelper(testHelper)
82+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
83+
84+
val e2eRoomID = cryptoTestData.roomId
85+
86+
// Alice
87+
val aliceSession = cryptoTestData.firstSession
88+
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
89+
90+
// Bob
91+
val bobSession = cryptoTestData.secondSession
92+
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
93+
94+
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
95+
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
96+
97+
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
98+
Assert.assertTrue("Message should be sent", aliceMessageId != null)
99+
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
100+
101+
// Bob should be able to decrypt the message
102+
testHelper.waitWithLatch { latch ->
103+
testHelper.retryPeriodicallyWithLatch(latch) {
104+
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.getTimelineEvent(aliceMessageId!!)
105+
(timelineEvent != null &&
106+
timelineEvent.isEncrypted() &&
107+
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
108+
if (it) {
109+
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
110+
}
111+
}
112+
}
113+
}
114+
115+
// Create a new user
116+
val arisSession = testHelper.createAccount("aris", SessionTestParams(true))
117+
Log.v("#E2E TEST", "Aris user created")
118+
119+
// Alice invites new user to the room
120+
testHelper.runBlockingTest {
121+
Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
122+
aliceRoomPOV.invite(arisSession.myUserId)
123+
}
124+
125+
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
126+
127+
ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper)
128+
Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID")
129+
130+
when (roomHistoryVisibility) {
131+
RoomHistoryVisibility.WORLD_READABLE,
132+
RoomHistoryVisibility.SHARED,
133+
null
134+
-> {
135+
// Aris should be able to decrypt the message
136+
testHelper.waitWithLatch { latch ->
137+
testHelper.retryPeriodicallyWithLatch(latch) {
138+
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.getTimelineEvent(aliceMessageId!!)
139+
(timelineEvent != null &&
140+
timelineEvent.isEncrypted() &&
141+
timelineEvent.root.getClearType() == EventType.MESSAGE
142+
).also {
143+
if (it) {
144+
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
145+
}
146+
}
147+
}
148+
}
149+
}
150+
RoomHistoryVisibility.INVITED,
151+
RoomHistoryVisibility.JOINED -> {
152+
// Aris should not even be able to get the message
153+
testHelper.waitWithLatch { latch ->
154+
testHelper.retryPeriodicallyWithLatch(latch) {
155+
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.getTimelineEvent(aliceMessageId!!)
156+
timelineEvent == null
157+
}
158+
}
159+
}
160+
}
161+
162+
testHelper.signOutAndClose(arisSession)
163+
cryptoTestData.cleanUp(testHelper)
164+
}
165+
166+
@Test
167+
fun testNeedsRotationFromWorldReadableToShared() {
168+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
169+
}
170+
171+
@Test
172+
fun testNeedsRotationFromWorldReadableToInvited() {
173+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
174+
}
175+
176+
@Test
177+
fun testNeedsRotationFromWorldReadableToJoined() {
178+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
179+
}
180+
181+
@Test
182+
fun testNeedsRotationFromSharedToWorldReadable() {
183+
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
184+
}
185+
186+
@Test
187+
fun testNeedsRotationFromSharedToInvited() {
188+
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited"))
189+
}
190+
191+
@Test
192+
fun testNeedsRotationFromSharedToJoined() {
193+
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined"))
194+
}
195+
196+
@Test
197+
fun testNeedsRotationFromInvitedToShared() {
198+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
199+
}
200+
201+
@Test
202+
fun testNeedsRotationFromInvitedToWorldReadable() {
203+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
204+
}
205+
206+
@Test
207+
fun testNeedsRotationFromInvitedToJoined() {
208+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
209+
}
210+
211+
@Test
212+
fun testNeedsRotationFromJoinedToShared() {
213+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
214+
}
215+
216+
@Test
217+
fun testNeedsRotationFromJoinedToInvited() {
218+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
219+
}
220+
221+
@Test
222+
fun testNeedsRotationFromJoinedToWorldReadable() {
223+
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
224+
}
225+
226+
/**
227+
* In this test we will test that a rotation is needed when
228+
* When the room's history visibility setting changes to world_readable or shared
229+
* from invited or joined, or changes to invited or joined from world_readable or shared,
230+
* senders that support this flag must rotate their megolm sessions.
231+
*/
232+
private fun testRotationDueToVisibilityChange(
233+
initRoomHistoryVisibility: RoomHistoryVisibility,
234+
nextRoomHistoryVisibility: RoomHistoryVisibilityContent
235+
) {
236+
val testHelper = CommonTestHelper(context())
237+
val cryptoTestHelper = CryptoTestHelper(testHelper)
238+
239+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
240+
val e2eRoomID = cryptoTestData.roomId
241+
242+
// Alice
243+
val aliceSession = cryptoTestData.firstSession
244+
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
245+
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
246+
247+
// Bob
248+
val bobSession = cryptoTestData.secondSession
249+
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
250+
251+
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
252+
Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID")
253+
254+
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
255+
Assert.assertTrue("Message should be sent", aliceMessageId != null)
256+
Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID")
257+
258+
// Bob should be able to decrypt the message
259+
testHelper.waitWithLatch { latch ->
260+
testHelper.retryPeriodicallyWithLatch(latch) {
261+
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.getTimelineEvent(aliceMessageId!!)
262+
(timelineEvent != null &&
263+
timelineEvent.isEncrypted() &&
264+
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
265+
if (it) {
266+
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
267+
}
268+
}
269+
}
270+
}
271+
272+
// Rotation has already been done so we do not need to rotate again
273+
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), false)
274+
Log.v("#E2E TEST ROTATION", "No rotation needed yet")
275+
276+
// Let's change the room history visibility
277+
testHelper.waitWithLatch {
278+
aliceRoomPOV.sendStateEvent(
279+
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
280+
stateKey = "",
281+
body = RoomHistoryVisibilityContent(_historyVisibility = nextRoomHistoryVisibility._historyVisibility).toContent()
282+
)
283+
it.countDown()
284+
}
285+
testHelper.waitWithLatch { latch ->
286+
testHelper.retryPeriodicallyWithLatch(latch) {
287+
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
288+
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)
289+
?.content
290+
?.toModel<RoomHistoryVisibilityContent>()
291+
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
292+
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
293+
}
294+
}
295+
296+
when {
297+
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
298+
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), false)
299+
Log.v("#E2E TEST ROTATION", "Rotation is not needed")
300+
}
301+
initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> {
302+
assertEquals(aliceCryptoStore.needsRotationDueToVisibilityChange(e2eRoomID), true)
303+
Log.v("#E2E TEST ROTATION", "Rotation is needed!")
304+
}
305+
}
306+
307+
cryptoTestData.cleanUp(testHelper)
308+
}
309+
310+
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
311+
aliceRoomPOV.sendTextMessage(text)
312+
var sentEventId: String? = null
313+
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
314+
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
315+
timeline.start()
316+
testHelper.retryPeriodicallyWithLatch(latch) {
317+
val decryptedMsg = timeline.getSnapshot()
318+
.filter { it.root.getClearType() == EventType.MESSAGE }
319+
.also { list ->
320+
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
321+
Log.v("#E2E TEST", "Timeline snapshot is $message")
322+
}
323+
.filter { it.root.sendState == SendState.SYNCED }
324+
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
325+
sentEventId = decryptedMsg?.eventId
326+
decryptedMsg != null
327+
}
328+
329+
timeline.dispose()
330+
}
331+
return sentEventId
332+
}
333+
334+
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
335+
testHelper.waitWithLatch { latch ->
336+
testHelper.retryPeriodicallyWithLatch(latch) {
337+
otherAccounts.map {
338+
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
339+
}.all {
340+
it == Membership.JOIN
341+
}
342+
}
343+
}
344+
}
345+
346+
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
347+
testHelper.waitWithLatch { latch ->
348+
testHelper.retryPeriodicallyWithLatch(latch) {
349+
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
350+
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
351+
if (it) {
352+
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
353+
}
354+
}
355+
}
356+
}
357+
358+
testHelper.runBlockingTest(60_000) {
359+
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
360+
try {
361+
otherSession.roomService().joinRoom(e2eRoomID)
362+
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
363+
// it's ok we will wait after
364+
}
365+
}
366+
367+
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
368+
testHelper.waitWithLatch {
369+
testHelper.retryPeriodicallyWithLatch(it) {
370+
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
371+
roomSummary != null && roomSummary.membership == Membership.JOIN
372+
}
373+
}
374+
}
375+
}

0 commit comments

Comments
 (0)