Merge pull request #8170 from vector-im/feature/fre/apply_push_rules_after_decryption

Reapply push rules on the decrypted event source (PSG-1146)
This commit is contained in:
Florian Renaud 2023-03-07 10:39:48 +01:00 committed by GitHub
commit 39c702f41b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 16 deletions

1
changelog.d/8170.bugfix Normal file
View file

@ -0,0 +1 @@
Reapply local push rules after event decryption

View file

@ -39,8 +39,17 @@ class EventMatchCondition(
override fun technicalDescription() = "'$key' matches '$pattern'" override fun technicalDescription() = "'$key' matches '$pattern'"
fun isSatisfied(event: Event): Boolean { fun isSatisfied(event: Event): Boolean {
// TODO encrypted events? val rawJson: Map<*, *> = (MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>)
val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *> ?.let { rawJson ->
val decryptedRawJson = event.mxDecryptionResult?.payload
if (decryptedRawJson != null) {
rawJson
.toMutableMap()
.apply { putAll(decryptedRawJson) }
} else {
rawJson
}
}
?: return false ?: return false
val value = extractField(rawJson, key) ?: return false val value = extractField(rawJson, key) ?: return false

View file

@ -94,7 +94,10 @@ internal class EventDecryptor @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
*/ */
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
tryOrNull(message = "Unable to decrypt the event") { // event is not encrypted or already decrypted
if (event.getClearType() != EventType.ENCRYPTED) return
tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") {
decryptEvent(event, timeline) decryptEvent(event, timeline)
} }
?.let { result -> ?.let { result ->

View file

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.isInvitation
import org.matrix.android.sdk.api.session.pushrules.PushEvents import org.matrix.android.sdk.api.session.pushrules.PushEvents
import org.matrix.android.sdk.api.session.pushrules.rest.PushRule import org.matrix.android.sdk.api.session.pushrules.rest.PushRule
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber import timber.log.Timber
@ -36,21 +37,22 @@ internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params
internal class DefaultProcessEventForPushTask @Inject constructor( internal class DefaultProcessEventForPushTask @Inject constructor(
private val defaultPushRuleService: DefaultPushRuleService, private val defaultPushRuleService: DefaultPushRuleService,
private val pushRuleFinder: PushRuleFinder, private val pushRuleFinder: PushRuleFinder,
@UserId private val userId: String @UserId private val userId: String,
private val eventDecryptor: EventDecryptor,
) : ProcessEventForPushTask { ) : ProcessEventForPushTask {
override suspend fun execute(params: ProcessEventForPushTask.Params) { override suspend fun execute(params: ProcessEventForPushTask.Params) {
val newJoinEvents = params.syncResponse.join val newJoinEvents = params.syncResponse.join
.mapNotNull { (key, value) -> .mapNotNull { (key, value) ->
value.timeline?.events?.mapNotNull { value.timeline?.events?.mapNotNull {
it.takeIf { !it.isInvitation() }?.copy(roomId = key) it.takeIf { !it.isInvitation() }?.copyAll(roomId = key)
} }
} }
.flatten() .flatten()
val inviteEvents = params.syncResponse.invite val inviteEvents = params.syncResponse.invite
.mapNotNull { (key, value) -> .mapNotNull { (key, value) ->
value.inviteState?.events?.map { it.copy(roomId = key) } value.inviteState?.events?.map { it.copyAll(roomId = key) }
} }
.flatten() .flatten()

View file

@ -437,6 +437,10 @@ internal class RoomSyncHandler @Inject constructor(
if (event.isEncrypted() && !isInitialSync) { if (event.isEncrypted() && !isInitialSync) {
try { try {
decryptIfNeeded(event, roomId) decryptIfNeeded(event, roomId)
// share the decryption result with the rawEvent because the decryption is done on a copy containing the roomId, see previous comment
rawEvent.mxDecryptionResult = event.mxDecryptionResult
rawEvent.mCryptoError = event.mCryptoError
rawEvent.mCryptoErrorReason = event.mCryptoErrorReason
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Timber.i("Decryption got interrupted") Timber.i("Decryption got interrupted")
} }

View file

@ -23,7 +23,11 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.MatrixTest import org.matrix.android.sdk.MatrixTest
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
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.members.MembershipService import org.matrix.android.sdk.api.session.room.members.MembershipService
@ -38,15 +42,40 @@ class PushRulesConditionTest : MatrixTest {
* Test EventMatchCondition * Test EventMatchCondition
* ========================================================================================== */ * ========================================================================================== */
private fun createFakeEncryptedEvent() = Event(
type = EventType.ENCRYPTED,
eventId = "mx0",
roomId = "!fakeRoom",
content = EncryptedEventContent(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...",
sessionId = "TO2G4u2HlnhtbIJk",
senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0",
deviceId = "FAKEE"
).toContent()
)
private fun createSimpleTextEvent(text: String): Event { private fun createSimpleTextEvent(text: String): Event {
return Event( return Event(
type = "m.room.message", type = EventType.MESSAGE,
eventId = "mx0", eventId = "mx0",
content = MessageTextContent("m.text", text).toContent(), content = MessageTextContent("m.text", text).toContent(),
originServerTs = 0 originServerTs = 0,
) )
} }
private fun createSimpleTextEventEncrypted(text: String): Event {
return createFakeEncryptedEvent().apply {
mxDecryptionResult = OlmDecryptionResult(
payload = mapOf(
"type" to EventType.MESSAGE,
"content" to MessageTextContent("m.text", text).toContent(),
),
senderKey = "the_real_sender_key",
)
}
}
@Test @Test
fun test_eventmatch_type_condition() { fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message") val condition = EventMatchCondition("type", "m.room.message")
@ -70,6 +99,26 @@ class PushRulesConditionTest : MatrixTest {
assertFalse(condition.isSatisfied(simpleRoomMemberEvent)) assertFalse(condition.isSatisfied(simpleRoomMemberEvent))
} }
@Test
fun test_decrypted_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message")
val simpleDecryptedTextEvent = createSimpleTextEventEncrypted("Yo wtf?")
val encryptedDummyEvent = createFakeEncryptedEvent().apply {
mxDecryptionResult = OlmDecryptionResult(
payload = mapOf(
"type" to EventType.DUMMY,
)
)
}
val encryptedEvent = createFakeEncryptedEvent()
assert(condition.isSatisfied(simpleDecryptedTextEvent))
assertFalse(condition.isSatisfied(encryptedDummyEvent))
assertFalse(condition.isSatisfied(encryptedEvent))
}
@Test @Test
fun test_eventmatch_path_condition() { fun test_eventmatch_path_condition() {
val condition = EventMatchCondition("content.msgtype", "m.text") val condition = EventMatchCondition("content.msgtype", "m.text")
@ -125,6 +174,22 @@ class PushRulesConditionTest : MatrixTest {
assert(condition.isSatisfied(createSimpleTextEvent("BEN"))) assert(condition.isSatisfied(createSimpleTextEvent("BEN")))
} }
@Test
fun test_encrypted_eventmatch_words_only_condition() {
val condition = EventMatchCondition("content.body", "ben")
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("benoit")))
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("Hello benoit")))
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("superben")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("ben")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello ben")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("ben is there")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello ben!")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("hello Ben!")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("BEN")))
}
@Test @Test
fun test_eventmatch_at_room_condition() { fun test_eventmatch_at_room_condition() {
val condition = EventMatchCondition("content.body", "@room") val condition = EventMatchCondition("content.body", "@room")
@ -140,6 +205,21 @@ class PushRulesConditionTest : MatrixTest {
assert(condition.isSatisfied(createSimpleTextEvent("Don't ping @room!"))) assert(condition.isSatisfied(createSimpleTextEvent("Don't ping @room!")))
} }
@Test
fun test_encrypted_eventmatch_at_room_condition() {
val condition = EventMatchCondition("content.body", "@room")
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("@roomba")))
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("room benoit")))
assertFalse(condition.isSatisfied(createSimpleTextEventEncrypted("abc@roomba")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("@room")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("@room, ben")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("@ROOM")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("Use:@room")))
assert(condition.isSatisfied(createSimpleTextEventEncrypted("Don't ping @room!")))
}
@Test @Test
fun test_notice_condition() { fun test_notice_condition() {
val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") val conditionEqual = EventMatchCondition("content.msgtype", "m.notice")
@ -155,6 +235,17 @@ class PushRulesConditionTest : MatrixTest {
} }
} }
@Test
fun test_eventmatch_encrypted_type_condition() {
val condition = EventMatchCondition("type", "m.room.encrypted")
val simpleDecryptedTextEvent = createSimpleTextEventEncrypted("Yo wtf?")
val encryptedEvent = createFakeEncryptedEvent()
assertFalse(condition.isSatisfied(simpleDecryptedTextEvent))
assert(condition.isSatisfied(encryptedEvent))
}
/* ========================================================================================== /* ==========================================================================================
* Test RoomMemberCountCondition * Test RoomMemberCountCondition
* ========================================================================================== */ * ========================================================================================== */

View file

@ -66,9 +66,6 @@ class NotifiableEventResolver @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) { ) {
private val nonEncryptedNotifiableEventTypes: List<String> =
listOf(EventType.MESSAGE) + EventType.POLL_START.values + EventType.POLL_END.values + EventType.STATE_ROOM_BEACON_INFO.values
suspend fun resolveEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? { suspend fun resolveEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? {
val roomID = event.roomId ?: return null val roomID = event.roomId ?: return null
val eventId = event.eventId ?: return null val eventId = event.eventId ?: return null
@ -76,9 +73,8 @@ class NotifiableEventResolver @Inject constructor(
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy) return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
} }
val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null
return when (event.getClearType()) { return when {
in nonEncryptedNotifiableEventTypes, event.supportsNotification() || event.type == EventType.ENCRYPTED -> {
EventType.ENCRYPTED -> {
resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
} }
else -> { else -> {
@ -163,8 +159,8 @@ class NotifiableEventResolver @Inject constructor(
} else { } else {
event.attemptToDecryptIfNeeded(session) event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents // only convert encrypted messages to NotifiableMessageEvents
when (event.root.getClearType()) { when {
in nonEncryptedNotifiableEventTypes -> { event.root.supportsNotification() -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName