Skip to content

Reapply push rules on the decrypted event source (PSG-1146) #8170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 7, 2023
1 change: 1 addition & 0 deletions changelog.d/8170.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reapply local push rules after event decryption
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@ class EventMatchCondition(
override fun technicalDescription() = "'$key' matches '$pattern'"

fun isSatisfied(event: Event): Boolean {
// TODO encrypted events?
val rawJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJsonValue(event) as? Map<*, *>
val rawJson: Map<*, *> = (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
val value = extractField(rawJson, key) ?: return false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
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)
}
?.let { result ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.rest.PushRule
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.task.Task
import timber.log.Timber
Expand All @@ -36,21 +37,22 @@ internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params
internal class DefaultProcessEventForPushTask @Inject constructor(
private val defaultPushRuleService: DefaultPushRuleService,
private val pushRuleFinder: PushRuleFinder,
@UserId private val userId: String
@UserId private val userId: String,
private val eventDecryptor: EventDecryptor,
) : ProcessEventForPushTask {

override suspend fun execute(params: ProcessEventForPushTask.Params) {
val newJoinEvents = params.syncResponse.join
.mapNotNull { (key, value) ->
value.timeline?.events?.mapNotNull {
it.takeIf { !it.isInvitation() }?.copy(roomId = key)
it.takeIf { !it.isInvitation() }?.copyAll(roomId = key)
}
}
.flatten()

val inviteEvents = params.syncResponse.invite
.mapNotNull { (key, value) ->
value.inviteState?.events?.map { it.copy(roomId = key) }
value.inviteState?.events?.map { it.copyAll(roomId = key) }
}
.flatten()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ internal class RoomSyncHandler @Inject constructor(
if (event.isEncrypted() && !isInitialSync) {
try {
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) {
Timber.i("Decryption got interrupted")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
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.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.room.Room
import org.matrix.android.sdk.api.session.room.members.MembershipService
Expand All @@ -38,15 +42,40 @@ class PushRulesConditionTest : MatrixTest {
* 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 {
return Event(
type = "m.room.message",
type = EventType.MESSAGE,
eventId = "mx0",
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
fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message")
Expand All @@ -70,6 +99,26 @@ class PushRulesConditionTest : MatrixTest {
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
fun test_eventmatch_path_condition() {
val condition = EventMatchCondition("content.msgtype", "m.text")
Expand Down Expand Up @@ -125,6 +174,22 @@ class PushRulesConditionTest : MatrixTest {
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
fun test_eventmatch_at_room_condition() {
val condition = EventMatchCondition("content.body", "@room")
Expand All @@ -140,6 +205,21 @@ class PushRulesConditionTest : MatrixTest {
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
fun test_notice_condition() {
val conditionEqual = EventMatchCondition("content.msgtype", "m.notice")
Expand All @@ -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
* ========================================================================================== */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,15 @@ class NotifiableEventResolver @Inject constructor(
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? {
val roomID = event.roomId ?: return null
val eventId = event.eventId ?: return null
if (event.getClearType() == EventType.STATE_ROOM_MEMBER) {
return resolveStateRoomEvent(event, session, canBeReplaced = false, isNoisy = isNoisy)
}
val timelineEvent = session.getRoom(roomID)?.getTimelineEvent(eventId) ?: return null
return when (event.getClearType()) {
in nonEncryptedNotifiableEventTypes,
EventType.ENCRYPTED -> {
return when {
event.supportsNotification() || event.type == EventType.ENCRYPTED -> {
resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy)
}
else -> {
Expand Down Expand Up @@ -163,8 +159,8 @@ class NotifiableEventResolver @Inject constructor(
} else {
event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents
when (event.root.getClearType()) {
in nonEncryptedNotifiableEventTypes -> {
when {
event.root.supportsNotification() -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
Expand Down