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

Dehydrated devices #8380

Merged
merged 28 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
52ddb79
initial version of dehydration
uhoreg Jul 24, 2020
e60a99d
run black
uhoreg Jul 27, 2020
0f9e402
add changelog
uhoreg Jul 27, 2020
b59bc66
minor fixes
uhoreg Aug 4, 2020
96d9fc3
maybe this will make lint happy?
uhoreg Aug 4, 2020
460ebc5
Merge remote-tracking branch 'origin/develop' into dehydration
uhoreg Aug 5, 2020
c7c8f28
newer version of dehydration proposal, add doc improvements and other…
uhoreg Aug 17, 2020
b7398f7
fix nonexistent reference
uhoreg Sep 3, 2020
355f123
implement alternative dehydration API
uhoreg Sep 22, 2020
037c201
remove unneeded table
uhoreg Sep 22, 2020
a84e491
move rest endpoint to devices and tweak endpoint names
uhoreg Sep 29, 2020
24405c7
some fixes
uhoreg Sep 30, 2020
bb7c732
invalidate cache and clear old device when setting new device ID
uhoreg Sep 30, 2020
6110a6a
run black
uhoreg Sep 30, 2020
7029d97
add unit tests
uhoreg Sep 30, 2020
90f42ad
lint and fix changelog
uhoreg Sep 30, 2020
3a27b74
Merge remote-tracking branch 'origin/develop' into dehydration2
uhoreg Sep 30, 2020
f63d6c0
adjust copyright headers
uhoreg Sep 30, 2020
2b30fc7
black
uhoreg Sep 30, 2020
8e5ef16
Apply suggestions from code review
uhoreg Oct 2, 2020
5935477
apply suggestions from review
uhoreg Oct 2, 2020
e69835e
Apply suggestions from code review
uhoreg Oct 5, 2020
b934fde
fix a couple incorrect names
uhoreg Oct 5, 2020
0b34ac0
update display name when dehydrating device
uhoreg Oct 5, 2020
a87cb40
black
uhoreg Oct 5, 2020
2a10d7a
fix comments
uhoreg Oct 5, 2020
f37f6a0
apply changes from review
uhoreg Oct 6, 2020
92ba279
Merge remote-tracking branch 'origin/develop' into dehydration2
uhoreg Oct 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8380.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for device dehydration (MSC2697).
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
75 changes: 73 additions & 2 deletions synapse/handlers/device.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 OpenMarket Ltd
# Copyright 2019 New Vector Ltd
# Copyright 2019 The Matrix.org Foundation C.I.C.
# Copyright 2019,2020 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.
Expand All @@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple

from synapse.api import errors
from synapse.api.constants import EventTypes
Expand All @@ -29,6 +29,7 @@
from synapse.logging.opentracing import log_kv, set_tag, trace
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.types import (
JsonDict,
StreamToken,
get_domain_from_id,
get_verify_key_from_cross_signing_key,
Expand Down Expand Up @@ -505,6 +506,76 @@ async def user_left_room(self, user, room_id):
# receive device updates. Mark this in DB.
await self.store.mark_remote_user_device_list_as_unsubscribed(user_id)

async def store_dehydrated_device(
self,
user_id: str,
device_data: JsonDict,
initial_device_display_name: Optional[str] = None,
) -> str:
"""Store a dehydrated device for a user. If the user had a previous
dehydrated device, it is removed.

Args:
user_id: the user that we are storing the device for
device_data: the dehydrated device information
initial_device_display_name: The display name to use for the device
Returns:
device id of the dehydrated device
"""
device_id = await self.check_device_registered(
user_id, None, initial_device_display_name,
)
old_device_id = await self.store.store_dehydrated_device(
user_id, device_id, device_data
)
if old_device_id is not None:
await self.delete_device(user_id, old_device_id)
return device_id

async def get_dehydrated_device(self, user_id: str) -> Tuple[str, JsonDict]:
"""Retrieve the information for a dehydrated device.

Args:
user_id: the user whose dehydrated device we are looking for
Returns:
a tuple whose first item is the device ID, and the second item is
the dehydrated device information
"""
return await self.store.get_dehydrated_device(user_id)

async def rehydrate_device(
self, user_id: str, access_token: str, device_id: str
) -> dict:
"""Process a rehydration request from the user.

Args:
user_id: the user who is rehydrating the device
access_token: the access token used for the request
device_id: the ID of the device that will be rehydrated
Returns:
a dict containing {"success": True}
"""
success = await self.store.remove_dehydrated_device(user_id, device_id)

if success:
# If the dehydrated device was successfully deleted (the device ID
# matched the stored dehydrated device), then modify the access
# token to use the dehydrated device's ID and destroy the old device ID
old_device_id = await self.store.set_device_for_access_token(
access_token, device_id
)
await self.store.delete_device(user_id, old_device_id)
await self.store.delete_e2e_keys_by_device(
user_id=user_id, device_id=old_device_id
)
clokep marked this conversation as resolved.
Show resolved Hide resolved

# tell everyone that the old device is gone
await self.notify_device_update(user_id, [old_device_id])

return {"success": True}
else:
raise errors.NotFoundError()
clokep marked this conversation as resolved.
Show resolved Hide resolved


def _update_device_from_client_ips(device, client_ips):
ip = client_ips.get((device["user_id"], device["device_id"]), {})
Expand Down
112 changes: 112 additions & 0 deletions synapse/rest/client/v2_alpha/devices.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2020 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.
Expand All @@ -21,6 +22,7 @@
assert_params_in_dict,
parse_json_object_from_request,
)
from synapse.http.site import SynapseRequest

from ._base import client_patterns, interactive_auth_handler

Expand Down Expand Up @@ -151,7 +153,117 @@ async def on_PUT(self, request, device_id):
return 200, {}


class DehydratedDeviceServlet(RestServlet):
"""Request or complete a rehydration request.

GET /org.matrix.msc2697.v2/dehydrated_device

HTTP/1.1 200 OK
Content-Type: application/json

{
"device_id": "dehydrated_device_id",
"device_data": {
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
"account": "dehydrated_device"
}
}

PUT /org.matrix.msc2697/dehydrated_device
Content-Type: application/json

{
"device_data": {
"algorithm": "org.matrix.msc2697.v1.dehydration.v1.olm",
"account": "dehydrated_device"
}
}

HTTP/1.1 200 OK
Content-Type: application/json

{
"device_id": "dehydrated_device_id"
}

"""

PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device")
clokep marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, hs):
super(DehydratedDeviceServlet, self).__init__()
clokep marked this conversation as resolved.
Show resolved Hide resolved
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()

async def on_GET(self, request: SynapseRequest):
requester = await self.auth.get_user_by_req(request, allow_guest=True)
clokep marked this conversation as resolved.
Show resolved Hide resolved
(
device_id,
dehydrated_device,
) = await self.device_handler.get_dehydrated_device(requester.user.to_string())
if dehydrated_device:
result = {"device_id": device_id, "device_data": dehydrated_device}
return (200, result)
else:
raise errors.NotFoundError()
clokep marked this conversation as resolved.
Show resolved Hide resolved

async def on_PUT(self, request: SynapseRequest):
submission = parse_json_object_from_request(request)
requester = await self.auth.get_user_by_req(request)

device_id = await self.device_handler.store_dehydrated_device(
requester.user.to_string(),
submission["device_data"],
clokep marked this conversation as resolved.
Show resolved Hide resolved
submission.get("initial_device_display_name", None),
)
return 200, {"device_id": device_id}


class ClaimDehydratedDeviceServlet(RestServlet):
"""Store a dehydrated device.

POST /org.matrix.msc2697.v2/dehydrated_device/claim
Content-Type: application/json

{
"device_id": "dehydrated_device_id"
}

HTTP/1.1 200 OK
Content-Type: application/json

{
"success": true,
}

"""

PATTERNS = client_patterns("/org.matrix.msc2697.v2/dehydrated_device/claim")

def __init__(self, hs):
super(ClaimDehydratedDeviceServlet, self).__init__()
clokep marked this conversation as resolved.
Show resolved Hide resolved
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()

async def on_POST(self, request: SynapseRequest):
requester = await self.auth.get_user_by_req(request, allow_guest=True)

submission = parse_json_object_from_request(request)

result = await self.device_handler.rehydrate_device(
requester.user.to_string(),
self.auth.get_access_token_from_request(request),
submission["device_id"],
)

return (200, result)


def register_servlets(hs, http_server):
DeleteDevicesRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
DeviceRestServlet(hs).register(http_server)
DehydratedDeviceServlet(hs).register(http_server)
ClaimDehydratedDeviceServlet(hs).register(http_server)
35 changes: 21 additions & 14 deletions synapse/rest/client/v2_alpha/keys.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2019 New Vector Ltd
# Copyright 2020 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.
Expand Down Expand Up @@ -67,6 +68,7 @@ def __init__(self, hs):
super().__init__()
self.auth = hs.get_auth()
self.e2e_keys_handler = hs.get_e2e_keys_handler()
self.device_handler = hs.get_device_handler()

@trace(opname="upload_keys")
async def on_POST(self, request, device_id):
Expand All @@ -78,20 +80,25 @@ async def on_POST(self, request, device_id):
# passing the device_id here is deprecated; however, we allow it
# for now for compatibility with older clients.
clokep marked this conversation as resolved.
Show resolved Hide resolved
if requester.device_id is not None and device_id != requester.device_id:
set_tag("error", True)
log_kv(
{
"message": "Client uploading keys for a different device",
"logged_in_id": requester.device_id,
"key_being_uploaded": device_id,
}
)
logger.warning(
"Client uploading keys for a different device "
"(logged in as %s, uploading for %s)",
requester.device_id,
device_id,
)
(
dehydrated_device_id,
_,
) = await self.device_handler.get_dehydrated_device(user_id)
if device_id != dehydrated_device_id:
set_tag("error", True)
log_kv(
{
"message": "Client uploading keys for a different device",
"logged_in_id": requester.device_id,
"key_being_uploaded": device_id,
}
)
logger.warning(
"Client uploading keys for a different device "
"(logged in as %s, uploading for %s)",
requester.device_id,
device_id,
)
else:
device_id = requester.device_id

Expand Down
Loading