Skip to content

Commit 502b12a

Browse files
authored
Merge pull request #7290 from vector-im/feature/bca/hinder_verification
E2ee dos not hinder verification
2 parents ec7c8c8 + fddedda commit 502b12a

File tree

5 files changed

+124
-4
lines changed

5 files changed

+124
-4
lines changed

changelog.d/6723.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Can't verify user when option to send keys to verified devices only is selected

matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
313313
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
314314
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
315315
}
316-
bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
316+
bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, incomingRequest.transactionId!!)
317317

318318
var requestID: String? = null
319319
// wait for it to be readied

matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import org.junit.runner.RunWith
3333
import org.junit.runners.JUnit4
3434
import org.junit.runners.MethodSorters
3535
import org.matrix.android.sdk.InstrumentedTest
36+
import org.matrix.android.sdk.api.auth.UIABaseAuth
37+
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
38+
import org.matrix.android.sdk.api.auth.UserPasswordAuth
39+
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
3640
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
3741
import org.matrix.android.sdk.api.session.Session
3842
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -61,7 +65,10 @@ import org.matrix.android.sdk.common.CommonTestHelper
6165
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
6266
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
6367
import org.matrix.android.sdk.common.SessionTestParams
68+
import org.matrix.android.sdk.common.TestConstants
6469
import org.matrix.android.sdk.mustFail
70+
import timber.log.Timber
71+
import kotlin.coroutines.Continuation
6572
import kotlin.coroutines.resume
6673

6774
// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
@@ -607,6 +614,85 @@ class E2eeSanityTests : InstrumentedTest {
607614
)
608615
}
609616

617+
@Test
618+
fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
619+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
620+
621+
val aliceSession = cryptoTestData.firstSession
622+
val bobSession = cryptoTestData.secondSession
623+
624+
val aliceAuthParams = UserPasswordAuth(
625+
user = aliceSession.myUserId,
626+
password = TestConstants.PASSWORD
627+
)
628+
val bobAuthParams = UserPasswordAuth(
629+
user = bobSession!!.myUserId,
630+
password = TestConstants.PASSWORD
631+
)
632+
633+
testHelper.waitForCallback {
634+
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
635+
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
636+
promise.resume(aliceAuthParams)
637+
}
638+
}, it)
639+
}
640+
641+
testHelper.waitForCallback {
642+
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
643+
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
644+
promise.resume(bobAuthParams)
645+
}
646+
}, it)
647+
}
648+
649+
// add a second session for bob but not cross signed
650+
651+
val secondBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
652+
653+
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
654+
655+
// The two bob session should not be able to decrypt any message
656+
657+
val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!!
658+
Timber.v("#TEST: Send a first message that should be withheld")
659+
val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!!
660+
661+
// wait for it to be synced back the other side
662+
Timber.v("#TEST: Wait for message to be synced back")
663+
testHelper.retryPeriodically {
664+
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
665+
}
666+
667+
testHelper.retryPeriodically {
668+
secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
669+
}
670+
671+
// bob should not be able to decrypt
672+
Timber.v("#TEST: Ensure cannot be decrytped")
673+
cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), bobSession, cryptoTestData.roomId)
674+
cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), secondBobSession, cryptoTestData.roomId)
675+
676+
// let's try to verify, it should work even if bob devices are untrusted
677+
Timber.v("#TEST: Do the verification")
678+
cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId)
679+
680+
Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt")
681+
682+
val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!!
683+
Timber.v("#TEST: Wait for message to be synced back")
684+
testHelper.retryPeriodically {
685+
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
686+
}
687+
688+
testHelper.retryPeriodically {
689+
secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
690+
}
691+
692+
cryptoTestHelper.ensureCanDecrypt(listOf(secondEvent), bobSession, cryptoTestData.roomId, listOf("World"))
693+
cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId)
694+
}
695+
610696
private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
611697
return scope.async {
612698
suspendCancellableCoroutine { continuation ->

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,17 @@ object EventType {
128128
type == CALL_REJECT ||
129129
type == CALL_REPLACES
130130
}
131+
132+
fun isVerificationEvent(type: String): Boolean {
133+
return when (type) {
134+
KEY_VERIFICATION_START,
135+
KEY_VERIFICATION_ACCEPT,
136+
KEY_VERIFICATION_KEY,
137+
KEY_VERIFICATION_MAC,
138+
KEY_VERIFICATION_CANCEL,
139+
KEY_VERIFICATION_DONE,
140+
KEY_VERIFICATION_READY -> true
141+
else -> false
142+
}
143+
}
131144
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import org.matrix.android.sdk.api.session.events.model.Content
3131
import org.matrix.android.sdk.api.session.events.model.EventType
3232
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
3333
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
34+
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
35+
import org.matrix.android.sdk.api.session.room.model.message.MessageType
3436
import org.matrix.android.sdk.internal.crypto.DeviceListManager
3537
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
3638
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -92,7 +94,18 @@ internal class MXMegolmEncryption(
9294
): Content {
9395
val ts = clock.epochMillis()
9496
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
95-
val devices = getDevicesInRoom(userIds)
97+
98+
/**
99+
* When using in-room messages and the room has encryption enabled,
100+
* clients should ensure that encryption does not hinder the verification.
101+
* For example, if the verification messages are encrypted, clients must ensure that all the recipient’s
102+
* unverified devices receive the keys necessary to decrypt the messages,
103+
* even if they would normally not be given the keys to decrypt messages in the room.
104+
*/
105+
val shouldSendToUnverified = isVerificationEvent(eventType, eventContent)
106+
107+
val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified)
108+
96109
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
97110
Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
98111
val outboundSession = ensureOutboundSession(devices.allowedDevices)
@@ -107,6 +120,11 @@ internal class MXMegolmEncryption(
107120
}
108121
}
109122

123+
private fun isVerificationEvent(eventType: String, eventContent: Content) =
124+
EventType.isVerificationEvent(eventType) ||
125+
(eventType == EventType.MESSAGE &&
126+
eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST)
127+
110128
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
111129
// offload to computation thread
112130
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
@@ -416,8 +434,10 @@ internal class MXMegolmEncryption(
416434
* This method must be called in getDecryptingThreadHandler() thread.
417435
*
418436
* @param userIds the user ids whose devices must be checked.
437+
* @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if
438+
* such devices are blocked in crypto settings
419439
*/
420-
private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo {
440+
private suspend fun getDevicesInRoom(userIds: List<String>, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo {
421441
// We are happy to use a cached version here: we assume that if we already
422442
// have a list of the user's devices, then we already share an e2e room
423443
// with them, which means that they will have announced any new devices via
@@ -444,7 +464,7 @@ internal class MXMegolmEncryption(
444464
continue
445465
}
446466

447-
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
467+
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) {
448468
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
449469
continue
450470
}

0 commit comments

Comments
 (0)