Skip to content

Commit 01533db

Browse files
authored
Merge pull request #7713 from vector-im/feature/bca/to_device_tracing
add to device tracing id
2 parents 6c94f1c + 63d2886 commit 01533db

File tree

5 files changed

+312
-6
lines changed

5 files changed

+312
-6
lines changed

changelog.d/7708.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tracing Id for to device messages

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.matrix.android.sdk.api.session.events.model
1818

19+
import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VERIFICATION_REQUEST
20+
1921
/**
2022
* Constants defining known event types from Matrix specifications.
2123
*/
@@ -126,6 +128,7 @@ object EventType {
126128

127129
fun isVerificationEvent(type: String): Boolean {
128130
return when (type) {
131+
MSGTYPE_VERIFICATION_REQUEST,
129132
KEY_VERIFICATION_START,
130133
KEY_VERIFICATION_ACCEPT,
131134
KEY_VERIFICATION_KEY,

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,32 @@
1717
package org.matrix.android.sdk.internal.crypto.tasks
1818

1919
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
20+
import org.matrix.android.sdk.api.session.events.model.Event
21+
import org.matrix.android.sdk.api.session.events.model.EventType
22+
import org.matrix.android.sdk.api.session.events.model.toContent
2023
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
2124
import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
2225
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
2326
import org.matrix.android.sdk.internal.network.executeRequest
2427
import org.matrix.android.sdk.internal.task.Task
28+
import timber.log.Timber
2529
import java.util.UUID
2630
import javax.inject.Inject
2731

32+
const val TO_DEVICE_TRACING_ID_KEY = "org.matrix.msgid"
33+
34+
fun Event.toDeviceTracingId(): String? = content?.get(TO_DEVICE_TRACING_ID_KEY) as? String
35+
2836
internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
2937
data class Params(
3038
// the type of event to send
3139
val eventType: String,
3240
// the content to send. Map from user_id to device_id to content dictionary.
3341
val contentMap: MXUsersDevicesMap<Any>,
3442
// the transactionId. If not provided, a transactionId will be created by the task
35-
val transactionId: String? = null
43+
val transactionId: String? = null,
44+
// add tracing id, notice that to device events that do signature on content might be broken by it
45+
val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType),
3646
)
3747
}
3848

@@ -42,15 +52,22 @@ internal class DefaultSendToDeviceTask @Inject constructor(
4252
) : SendToDeviceTask {
4353

4454
override suspend fun execute(params: SendToDeviceTask.Params) {
45-
val sendToDeviceBody = SendToDeviceBody(
46-
messages = params.contentMap.map
47-
)
48-
4955
// If params.transactionId is not provided, we create a unique txnId.
5056
// It's important to do that outside the requestBlock parameter of executeRequest()
5157
// to use the same value if the request is retried
5258
val txnId = params.transactionId ?: createUniqueTxnId()
5359

60+
// add id tracing to debug
61+
val decorated = if (params.addTracingIds) {
62+
decorateWithToDeviceTracingIds(params)
63+
} else {
64+
params.contentMap.map to emptyList()
65+
}
66+
67+
val sendToDeviceBody = SendToDeviceBody(
68+
messages = decorated.first
69+
)
70+
5471
return executeRequest(
5572
globalErrorReceiver,
5673
canRetry = true,
@@ -61,8 +78,35 @@ internal class DefaultSendToDeviceTask @Inject constructor(
6178
transactionId = txnId,
6279
body = sendToDeviceBody
6380
)
81+
Timber.i("Sent to device type=${params.eventType} txnid=$txnId [${decorated.second.joinToString(",")}]")
6482
}
6583
}
84+
85+
/**
86+
* To make it easier to track down where to-device messages are getting lost,
87+
* add a custom property to each one, and that will be logged after sent and on reception. Synapse will also log
88+
* this property.
89+
* @return A pair, first is the decorated content, and second info to log out after sending
90+
*/
91+
private fun decorateWithToDeviceTracingIds(params: SendToDeviceTask.Params): Pair<Map<String, Map<String, Any>>, List<String>> {
92+
val tracingInfo = mutableListOf<String>()
93+
val decoratedContent = params.contentMap.map.map { userToDeviceMap ->
94+
val userId = userToDeviceMap.key
95+
userId to userToDeviceMap.value.map {
96+
val deviceId = it.key
97+
deviceId to it.value.toContent().toMutableMap().apply {
98+
put(
99+
TO_DEVICE_TRACING_ID_KEY,
100+
UUID.randomUUID().toString().also {
101+
tracingInfo.add("$userId/$deviceId (msgid $it)")
102+
}
103+
)
104+
}
105+
}.toMap()
106+
}.toMap()
107+
108+
return decoratedContent to tracingInfo
109+
}
66110
}
67111

68112
internal fun createUniqueTxnId() = UUID.randomUUID().toString()

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
2929
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
3030
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
3131
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
32+
import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId
3233
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
3334
import org.matrix.android.sdk.internal.session.sync.ProgressReporter
3435
import timber.log.Timber
@@ -48,12 +49,14 @@ internal class CryptoSyncHandler @Inject constructor(
4849
?.forEachIndexed { index, event ->
4950
progressReporter?.reportProgress(index * 100F / total)
5051
// Decrypt event if necessary
51-
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
52+
Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}")
5253
decryptToDeviceEvent(event, null)
54+
5355
if (event.getClearType() == EventType.MESSAGE &&
5456
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
5557
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
5658
} else {
59+
Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}")
5760
verificationService.onToDeviceEvent(event)
5861
cryptoService.onToDeviceEvent(event)
5962
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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 io.mockk.mockk
20+
import kotlinx.coroutines.runBlocking
21+
import org.amshove.kluent.internal.assertEquals
22+
import org.junit.Assert
23+
import org.junit.Test
24+
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
25+
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
26+
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
27+
import org.matrix.android.sdk.api.session.events.model.EventType
28+
import org.matrix.android.sdk.api.session.room.model.message.MessageType
29+
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
30+
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
31+
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
32+
import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
33+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
34+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
35+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody
36+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
37+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
38+
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
39+
import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
40+
import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse
41+
import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody
42+
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
43+
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
44+
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
45+
46+
class DefaultSendToDeviceTaskTest {
47+
48+
private val users = listOf(
49+
"@alice:example.com" to listOf("D0", "D1"),
50+
"bob@example.com" to listOf("D2", "D3")
51+
)
52+
53+
private val fakeEncryptedContent = mapOf(
54+
"algorithm" to "m.olm.v1.curve25519-aes-sha2",
55+
"sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw",
56+
"ciphertext" to mapOf(
57+
"tdwXf7006FDgzmufMCVI4rDdVPO51ecRTTT6HkRxUwE" to mapOf(
58+
"type" to 0,
59+
"body" to "AwogCA1ULEc0abGIFxMDIC9iv7ul3jqJSnapTHQ+8JJx"
60+
)
61+
)
62+
)
63+
64+
private val fakeStartVerificationContent = mapOf(
65+
"method" to "m.sas.v1",
66+
"from_device" to "MNQHVEISFQ",
67+
"key_agreement_protocols" to listOf(
68+
"curve25519-hkdf-sha256",
69+
"curve25519"
70+
),
71+
"hashes" to listOf("sha256"),
72+
"message_authentication_codes" to listOf(
73+
"org.matrix.msc3783.hkdf-hmac-sha256",
74+
"hkdf-hmac-sha256",
75+
"hmac-sha256"
76+
),
77+
"short_authentication_string" to listOf(
78+
"decimal",
79+
"emoji"
80+
),
81+
"transaction_id" to "4wNOpkHGwGZPXjkZToooCDWfb8hsf7vW"
82+
)
83+
84+
@Test
85+
fun `tracing id should be added to to_device contents`() {
86+
val fakeCryptoAPi = FakeCryptoApi()
87+
88+
val sendToDeviceTask = DefaultSendToDeviceTask(
89+
cryptoApi = fakeCryptoAPi,
90+
globalErrorReceiver = mockk(relaxed = true)
91+
)
92+
93+
val contentMap = MXUsersDevicesMap<Any>()
94+
95+
users.forEach { pairOfUserDevices ->
96+
val userId = pairOfUserDevices.first
97+
pairOfUserDevices.second.forEach {
98+
contentMap.setObject(userId, it, fakeEncryptedContent)
99+
}
100+
}
101+
102+
val params = SendToDeviceTask.Params(
103+
eventType = EventType.ENCRYPTED,
104+
contentMap = contentMap
105+
)
106+
107+
runBlocking {
108+
sendToDeviceTask.execute(params)
109+
}
110+
111+
val generatedIds = mutableListOf<String>()
112+
users.forEach { pairOfUserDevices ->
113+
val userId = pairOfUserDevices.first
114+
pairOfUserDevices.second.forEach {
115+
val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map<*, *>
116+
Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"])
117+
generatedIds.add(modifiedContent["org.matrix.msgid"] as String)
118+
119+
assertEquals(
120+
"The rest of the content should be the same",
121+
fakeEncryptedContent.keys,
122+
modifiedContent.toMutableMap().apply { remove("org.matrix.msgid") }.keys
123+
)
124+
}
125+
}
126+
127+
assertEquals("Id should be unique per content", generatedIds.size, generatedIds.toSet().size)
128+
println("modified content ${fakeCryptoAPi.body}")
129+
}
130+
131+
@Test
132+
fun `tracing id should not be added to verification start to_device contents`() {
133+
val fakeCryptoAPi = FakeCryptoApi()
134+
135+
val sendToDeviceTask = DefaultSendToDeviceTask(
136+
cryptoApi = fakeCryptoAPi,
137+
globalErrorReceiver = mockk(relaxed = true)
138+
)
139+
val contentMap = MXUsersDevicesMap<Any>()
140+
contentMap.setObject("@alice:example.com", "MNQHVEISFQ", fakeStartVerificationContent)
141+
142+
val params = SendToDeviceTask.Params(
143+
eventType = EventType.KEY_VERIFICATION_START,
144+
contentMap = contentMap
145+
)
146+
147+
runBlocking {
148+
sendToDeviceTask.execute(params)
149+
}
150+
151+
val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
152+
Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
153+
154+
// try to force
155+
runBlocking {
156+
sendToDeviceTask.execute(
157+
SendToDeviceTask.Params(
158+
eventType = EventType.KEY_VERIFICATION_START,
159+
contentMap = contentMap,
160+
addTracingIds = true
161+
)
162+
)
163+
}
164+
165+
val modifiedContentForced = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
166+
Assert.assertNotNull("Tracing id should have been added", modifiedContentForced["org.matrix.msgid"])
167+
}
168+
169+
@Test
170+
fun `tracing id should not be added to all verification to_device contents`() {
171+
val fakeCryptoAPi = FakeCryptoApi()
172+
173+
val sendToDeviceTask = DefaultSendToDeviceTask(
174+
cryptoApi = fakeCryptoAPi,
175+
globalErrorReceiver = mockk(relaxed = true)
176+
)
177+
val contentMap = MXUsersDevicesMap<Any>()
178+
contentMap.setObject("@alice:example.com", "MNQHVEISFQ", emptyMap<String, Any>())
179+
180+
val verificationEvents = listOf(
181+
MessageType.MSGTYPE_VERIFICATION_REQUEST,
182+
EventType.KEY_VERIFICATION_START,
183+
EventType.KEY_VERIFICATION_ACCEPT,
184+
EventType.KEY_VERIFICATION_KEY,
185+
EventType.KEY_VERIFICATION_MAC,
186+
EventType.KEY_VERIFICATION_CANCEL,
187+
EventType.KEY_VERIFICATION_DONE,
188+
EventType.KEY_VERIFICATION_READY
189+
)
190+
191+
for (type in verificationEvents) {
192+
val params = SendToDeviceTask.Params(
193+
eventType = type,
194+
contentMap = contentMap
195+
)
196+
runBlocking {
197+
sendToDeviceTask.execute(params)
198+
}
199+
200+
val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
201+
Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
202+
}
203+
}
204+
205+
internal class FakeCryptoApi : CryptoApi {
206+
override suspend fun getDevices(): DevicesListResponse {
207+
throw java.lang.AssertionError("Should not be called")
208+
}
209+
210+
override suspend fun getDeviceInfo(deviceId: String): DeviceInfo {
211+
throw java.lang.AssertionError("Should not be called")
212+
}
213+
214+
override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse {
215+
throw java.lang.AssertionError("Should not be called")
216+
}
217+
218+
override suspend fun downloadKeysForUsers(params: KeysQueryBody): KeysQueryResponse {
219+
throw java.lang.AssertionError("Should not be called")
220+
}
221+
222+
override suspend fun uploadSigningKeys(params: UploadSigningKeysBody): KeysQueryResponse {
223+
throw java.lang.AssertionError("Should not be called")
224+
}
225+
226+
override suspend fun uploadSignatures(params: Map<String, Any>?): SignatureUploadResponse {
227+
throw java.lang.AssertionError("Should not be called")
228+
}
229+
230+
override suspend fun claimOneTimeKeysForUsersDevices(body: KeysClaimBody): KeysClaimResponse {
231+
throw java.lang.AssertionError("Should not be called")
232+
}
233+
234+
var body: SendToDeviceBody? = null
235+
override suspend fun sendToDevice(eventType: String, transactionId: String, body: SendToDeviceBody) {
236+
this.body = body
237+
}
238+
239+
override suspend fun deleteDevice(deviceId: String, params: DeleteDeviceParams) {
240+
throw java.lang.AssertionError("Should not be called")
241+
}
242+
243+
override suspend fun deleteDevices(params: DeleteDevicesParams) {
244+
throw java.lang.AssertionError("Should not be called")
245+
}
246+
247+
override suspend fun updateDeviceInfo(deviceId: String, params: UpdateDeviceInfoBody) {
248+
throw java.lang.AssertionError("Should not be called")
249+
}
250+
251+
override suspend fun getKeyChanges(oldToken: String, newToken: String): KeyChangesResponse {
252+
throw java.lang.AssertionError("Should not be called")
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)