Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 28bceef

Browse files
authored
Check appservices for devices during a /user/devices query. (#15539)
MSC3984 proxies /keys/query requests to appservices, but servers will can also requests devices / keys from the /user/devices endpoint. The formats are close enough that we can "proxy" that /user/devices to appservices (by calling /keys/query) and then change the format of the returned data before returning it over federation.
1 parent 36df9c5 commit 28bceef

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

changelog.d/15539.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Proxy `/user/devices` federation queries to application services for [MSC3984](https://github.com/matrix-org/matrix-spec-proposals/pull/3984).

synapse/handlers/device.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ def __init__(self, hs: "HomeServer"):
7575
self.store = hs.get_datastores().main
7676
self.notifier = hs.get_notifier()
7777
self.state = hs.get_state_handler()
78+
self._appservice_handler = hs.get_application_service_handler()
7879
self._state_storage = hs.get_storage_controllers().state
7980
self._auth_handler = hs.get_auth_handler()
8081
self.server_name = hs.hostname
8182
self._msc3852_enabled = hs.config.experimental.msc3852_enabled
83+
self._query_appservices_for_keys = (
84+
hs.config.experimental.msc3984_appservice_key_query
85+
)
8286

8387
self.device_list_updater = DeviceListWorkerUpdater(hs)
8488

@@ -328,6 +332,30 @@ async def on_federation_query_user_devices(self, user_id: str) -> JsonDict:
328332
user_id, "self_signing"
329333
)
330334

335+
# Check if the application services have any results.
336+
if self._query_appservices_for_keys:
337+
# Query the appservice for all devices for this user.
338+
query: Dict[str, Optional[List[str]]] = {user_id: None}
339+
340+
# Query the appservices for any keys.
341+
appservice_results = await self._appservice_handler.query_keys(query)
342+
343+
# Merge results, overriding anything from the database.
344+
appservice_devices = appservice_results.get("device_keys", {}).get(
345+
user_id, {}
346+
)
347+
348+
# Filter the database results to only those devices that the appservice has
349+
# *not* responded with.
350+
devices = [d for d in devices if d["device_id"] not in appservice_devices]
351+
# Append the appservice response by wrapping each result in another dictionary.
352+
devices.extend(
353+
{"device_id": device_id, "keys": device}
354+
for device_id, device in appservice_devices.items()
355+
)
356+
357+
# TODO Handle cross-signing keys.
358+
331359
return {
332360
"user_id": user_id,
333361
"stream_id": stream_id,

tests/handlers/test_device.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,35 @@
1515
# limitations under the License.
1616

1717
from typing import Optional
18+
from unittest import mock
1819

1920
from twisted.test.proto_helpers import MemoryReactor
2021

22+
from synapse.api.constants import RoomEncryptionAlgorithms
2123
from synapse.api.errors import NotFoundError, SynapseError
24+
from synapse.appservice import ApplicationService
2225
from synapse.handlers.device import MAX_DEVICE_DISPLAY_NAME_LEN, DeviceHandler
2326
from synapse.server import HomeServer
27+
from synapse.storage.databases.main.appservice import _make_exclusive_regex
28+
from synapse.types import JsonDict
2429
from synapse.util import Clock
2530

2631
from tests import unittest
32+
from tests.test_utils import make_awaitable
33+
from tests.unittest import override_config
2734

2835
user1 = "@boris:aaa"
2936
user2 = "@theresa:bbb"
3037

3138

3239
class DeviceTestCase(unittest.HomeserverTestCase):
3340
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
34-
hs = self.setup_test_homeserver("server", federation_http_client=None)
41+
self.appservice_api = mock.Mock()
42+
hs = self.setup_test_homeserver(
43+
"server",
44+
federation_http_client=None,
45+
application_service_api=self.appservice_api,
46+
)
3547
handler = hs.get_device_handler()
3648
assert isinstance(handler, DeviceHandler)
3749
self.handler = handler
@@ -265,6 +277,127 @@ def _record_user(
265277
)
266278
self.reactor.advance(1000)
267279

280+
@override_config({"experimental_features": {"msc3984_appservice_key_query": True}})
281+
def test_on_federation_query_user_devices_appservice(self) -> None:
282+
"""Test that querying of appservices for keys overrides responses from the database."""
283+
local_user = "@boris:" + self.hs.hostname
284+
device_1 = "abc"
285+
device_2 = "def"
286+
device_3 = "ghi"
287+
288+
# There are 3 devices:
289+
#
290+
# 1. One which is uploaded to the homeserver.
291+
# 2. One which is uploaded to the homeserver, but a newer copy is returned
292+
# by the appservice.
293+
# 3. One which is only returned by the appservice.
294+
device_key_1: JsonDict = {
295+
"user_id": local_user,
296+
"device_id": device_1,
297+
"algorithms": [
298+
"m.olm.curve25519-aes-sha2",
299+
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
300+
],
301+
"keys": {
302+
"ed25519:abc": "base64+ed25519+key",
303+
"curve25519:abc": "base64+curve25519+key",
304+
},
305+
"signatures": {local_user: {"ed25519:abc": "base64+signature"}},
306+
}
307+
device_key_2a: JsonDict = {
308+
"user_id": local_user,
309+
"device_id": device_2,
310+
"algorithms": [
311+
"m.olm.curve25519-aes-sha2",
312+
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
313+
],
314+
"keys": {
315+
"ed25519:def": "base64+ed25519+key",
316+
"curve25519:def": "base64+curve25519+key",
317+
},
318+
"signatures": {local_user: {"ed25519:def": "base64+signature"}},
319+
}
320+
321+
device_key_2b: JsonDict = {
322+
"user_id": local_user,
323+
"device_id": device_2,
324+
"algorithms": [
325+
"m.olm.curve25519-aes-sha2",
326+
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
327+
],
328+
# The device ID is the same (above), but the keys are different.
329+
"keys": {
330+
"ed25519:xyz": "base64+ed25519+key",
331+
"curve25519:xyz": "base64+curve25519+key",
332+
},
333+
"signatures": {local_user: {"ed25519:xyz": "base64+signature"}},
334+
}
335+
device_key_3: JsonDict = {
336+
"user_id": local_user,
337+
"device_id": device_3,
338+
"algorithms": [
339+
"m.olm.curve25519-aes-sha2",
340+
RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
341+
],
342+
"keys": {
343+
"ed25519:jkl": "base64+ed25519+key",
344+
"curve25519:jkl": "base64+curve25519+key",
345+
},
346+
"signatures": {local_user: {"ed25519:jkl": "base64+signature"}},
347+
}
348+
349+
# Upload keys for devices 1 & 2a.
350+
e2e_keys_handler = self.hs.get_e2e_keys_handler()
351+
self.get_success(
352+
e2e_keys_handler.upload_keys_for_user(
353+
local_user, device_1, {"device_keys": device_key_1}
354+
)
355+
)
356+
self.get_success(
357+
e2e_keys_handler.upload_keys_for_user(
358+
local_user, device_2, {"device_keys": device_key_2a}
359+
)
360+
)
361+
362+
# Inject an appservice interested in this user.
363+
appservice = ApplicationService(
364+
token="i_am_an_app_service",
365+
id="1234",
366+
namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
367+
# Note: this user does not have to match the regex above
368+
sender="@as_main:test",
369+
)
370+
self.hs.get_datastores().main.services_cache = [appservice]
371+
self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
372+
[appservice]
373+
)
374+
375+
# Setup a response.
376+
self.appservice_api.query_keys.return_value = make_awaitable(
377+
{
378+
"device_keys": {
379+
local_user: {device_2: device_key_2b, device_3: device_key_3}
380+
}
381+
}
382+
)
383+
384+
# Request all devices.
385+
res = self.get_success(
386+
self.handler.on_federation_query_user_devices(local_user)
387+
)
388+
self.assertIn("devices", res)
389+
res_devices = res["devices"]
390+
for device in res_devices:
391+
device["keys"].pop("unsigned", None)
392+
self.assertEqual(
393+
res_devices,
394+
[
395+
{"device_id": device_1, "keys": device_key_1},
396+
{"device_id": device_2, "keys": device_key_2b},
397+
{"device_id": device_3, "keys": device_key_3},
398+
],
399+
)
400+
268401

269402
class DehydrationTestCase(unittest.HomeserverTestCase):
270403
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:

0 commit comments

Comments
 (0)