Skip to content

Commit

Permalink
Merge pull request #4726 from vector-im/feature/bca/proper_encryption…
Browse files Browse the repository at this point in the history
…_state

Support misconfigured room encryption
  • Loading branch information
bmarty authored Jan 11, 2022
2 parents a954a41 + 60ae416 commit 67bdf4b
Show file tree
Hide file tree
Showing 58 changed files with 413 additions and 99 deletions.
1 change: 1 addition & 0 deletions changelog.d/4711.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Better handling of misconfigured room encryption
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ enum class RoomEncryptionTrustLevel {
Warning,

// All devices in the room are verified -> the app should display a green shield
Trusted
Trusted,

// e2e is active but with an unsupported algorithm
E2EWithUnsupportedAlgorithm
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ interface RoomCryptoService {
fun shouldEncryptForInvitedMembers(): Boolean

/**
* Enable encryption of the room
* Enable encryption of the room.
* @param Use force to ensure that this algorithm will be used. Otherwise this call
* will throw if encryption is already setup or if the algorithm is not supported. Only to
* be used by admins to fix misconfigured encryption.
*/
suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM)
suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, force: Boolean = false)

/**
* Ensures all members of the room are loaded and outbound session keys are shared.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2021 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.api.session.room.model

import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM

sealed class RoomEncryptionAlgorithm {

abstract class SupportedAlgorithm(val alg: String) : RoomEncryptionAlgorithm()

object Megolm : SupportedAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM)

data class UnsupportedAlgorithm(val name: String?) : RoomEncryptionAlgorithm()
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ data class RoomSummary(
val roomType: String? = null,
val spaceParents: List<SpaceParentInfo>? = null,
val spaceChildren: List<SpaceChildInfo>? = null,
val flattenParentIds: List<String> = emptyList()
val flattenParentIds: List<String> = emptyList(),
val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null
) {

val isVersioned: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ internal class CryptoSessionInfoProvider @Inject constructor(
fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isEmpty(EventEntityFields.STATE_KEY)
.findFirst()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,21 @@ internal class DefaultCryptoService @Inject constructor(
private val isStarted = AtomicBoolean(false)

fun onStateEvent(roomId: String, event: Event) {
when (event.getClearType()) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}

fun onLiveEvent(roomId: String, event: Event) {
when (event.getClearType()) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
// handle state events
if (event.isStateEvent()) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
}

Expand Down Expand Up @@ -575,26 +578,31 @@ internal class DefaultCryptoService @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)

if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
if (existingAlgorithm == algorithm) {
// ignore
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
return false
}

val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)

// Always store even if not supported
cryptoStore.storeRoomAlgorithm(roomId, algorithm)

if (!encryptingClass) {
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
return false
}

cryptoStore.storeRoomAlgorithm(roomId, algorithm!!)

val alg: IMXEncrypting = when (algorithm) {
val alg: IMXEncrypting? = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
else -> olmEncryptionFactory.create(roomId)
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
else -> null
}

roomEncryptorsStore.put(roomId, alg)
if (alg != null) {
roomEncryptorsStore.put(roomId, alg)
}

// if encryption was not previously enabled in this room, we will have been
// ignoring new device events for these users so far. We may well have
Expand Down Expand Up @@ -927,6 +935,7 @@ internal class DefaultCryptoService @Inject constructor(
}

private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
eventContent?.historyVisibility?.let {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ data class EncryptionEventContent(
* Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
*/
@Json(name = "algorithm")
val algorithm: String,
val algorithm: String?,

/**
* How long the session should be used before changing it. 604800000 (a week) is the recommended default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ internal interface IMXCryptoStore {
* @param roomId the id of the room.
* @param algorithm the algorithm.
*/
fun storeRoomAlgorithm(roomId: String, algorithm: String)
fun storeRoomAlgorithm(roomId: String, algorithm: String?)

/**
* Provides the algorithm used in a dedicated room.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}

override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
Expand Down Expand Up @@ -55,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration {

companion object {
const val SESSION_STORE_SCHEMA_VERSION = 20L
const val SESSION_STORE_SCHEMA_VERSION = 21L
}

/**
Expand Down Expand Up @@ -88,6 +89,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion <= 17) migrateTo18(realm)
if (oldVersion <= 18) migrateTo19(realm)
if (oldVersion <= 19) migrateTo20(realm)
if (oldVersion <= 20) migrateTo21(realm)
}

private fun migrateTo1(realm: DynamicRealm) {
Expand Down Expand Up @@ -395,6 +397,7 @@ internal class RealmSessionStoreMigration @Inject constructor(

private fun migrateTo20(realm: DynamicRealm) {
Timber.d("Step 19 -> 20")

realm.schema.get("ChunkEntity")?.apply {
if (hasField("numberOfTimelineEvents")) {
removeField("numberOfTimelineEvents")
Expand All @@ -414,4 +417,32 @@ internal class RealmSessionStoreMigration @Inject constructor(
}
}
}

private fun migrateTo21(realm: DynamicRealm) {
Timber.d("Step 20 -> 21")

realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java)
?.transform { obj ->

val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java)

val encryptionEvent = realm.where("CurrentStateEventEntity")
.equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
.equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
.findFirst()

val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
val algorithm = encryptionEventRoot
?.getString(EventEntityFields.CONTENT)?.let {
encryptionContentAdapter.fromJson(it)?.algorithm
}

obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm)
obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null)
encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@

package org.matrix.android.sdk.internal.database.mapper

import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
import javax.inject.Inject
Expand Down Expand Up @@ -68,7 +71,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
isEncrypted = roomSummaryEntity.isEncrypted,
encryptionEventTs = roomSummaryEntity.encryptionEventTs,
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
roomEncryptionTrustLevel = if (roomSummaryEntity.isEncrypted && roomSummaryEntity.e2eAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm
} else roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending,
roomType = roomSummaryEntity.roomType,
Expand Down Expand Up @@ -99,7 +104,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
)
},
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
// I should probably use #hasEncryptorClassForAlgorithm but it says it supports
// OLM which is some legacy? Now only megolm allowed in rooms
MXCRYPTO_ALGORITHM_MEGOLM -> RoomEncryptionAlgorithm.Megolm
else -> RoomEncryptionAlgorithm.UnsupportedAlgorithm(alg)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ internal open class RoomSummaryEntity(
if (value != field) field = value
}

var e2eAlgorithm: String? = null
set(value) {
if (value != field) field = value
}

var encryptionEventTs: Long? = 0
set(value) {
if (value != field) field = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ internal class DefaultRoom(override val roomId: String,
}
}

override suspend fun enableEncryption(algorithm: String) {
override suspend fun enableEncryption(algorithm: String, force: Boolean) {
when {
isEncrypted() -> {
(!force && isEncrypted() && encryptionAlgorithm() == MXCRYPTO_ALGORITHM_MEGOLM) -> {
throw IllegalStateException("Encryption is already enabled for this room")
}
algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> {
(!force && algorithm != MXCRYPTO_ALGORITHM_MEGOLM) -> {
throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")
}
else -> {
else -> {
val params = SendStateTask.Params(
roomId = roomId,
stateKey = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
Expand All @@ -57,7 +55,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.isEventRead
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.query.process
Expand Down Expand Up @@ -123,10 +120,8 @@ internal class RoomSummaryUpdater @Inject constructor(
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")

// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isNotNull(EventEntityFields.STATE_KEY)
.findFirst()
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")

val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)

Expand All @@ -152,6 +147,11 @@ internal class RoomSummaryUpdater @Inject constructor(
.orEmpty()
roomSummaryEntity.updateAliases(roomAliases)
roomSummaryEntity.isEncrypted = encryptionEvent != null

roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content)
?.toModel<EncryptionEventContent>()
?.algorithm

roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs

if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
}
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
Timber.v("## received state event ${event.type} and key ${event.stateKey}")
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
// Timber.v("## Space state event: $eventEntity")
eventId = event.eventId
Expand Down Expand Up @@ -393,6 +394,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator)
}
}

roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
Expand Down
Loading

0 comments on commit 67bdf4b

Please sign in to comment.