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

Commit

Permalink
Add support for MSC2697: Dehydrated devices (#8380)
Browse files Browse the repository at this point in the history
This allows a user to store an offline device on the server and
then restore it at a subsequent login.
  • Loading branch information
uhoreg authored Oct 7, 2020
1 parent 43c6228 commit 4cb44a1
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 21 deletions.
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](https://github.com/matrix-org/matrix-doc/pull/2697)).
84 changes: 82 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,85 @@ 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
) -> Optional[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 not success:
raise errors.NotFoundError()

# 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 copy the old device
# display name to the dehydrated device, and destroy the old device
# ID
old_device_id = await self.store.set_device_for_access_token(
access_token, device_id
)
old_device = await self.store.get_device(user_id, old_device_id)
await self.store.update_device(user_id, device_id, old_device["display_name"])
# can't call self.delete_device because that will clobber the
# access token so call the storage layer directly
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
)

# tell everyone that the old device is gone and that the dehydrated
# device has a new display name
await self.notify_device_update(user_id, [old_device_id, device_id])

return {"success": True}


def _update_device_from_client_ips(device, client_ips):
ip = client_ips.get((device["user_id"], device["device_id"]), {})
Expand Down
134 changes: 134 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,139 @@ async def on_PUT(self, request, device_id):
return 200, {}


class DehydratedDeviceServlet(RestServlet):
"""Retrieve or store a dehydrated device.
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", releases=())

def __init__(self, hs):
super().__init__()
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)
dehydrated_device = await self.device_handler.get_dehydrated_device(
requester.user.to_string()
)
if dehydrated_device is not None:
(device_id, device_data) = dehydrated_device
result = {"device_id": device_id, "device_data": device_data}
return (200, result)
else:
raise errors.NotFoundError("No dehydrated device available")

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

if "device_data" not in submission:
raise errors.SynapseError(
400, "device_data missing", errcode=errors.Codes.MISSING_PARAM,
)
elif not isinstance(submission["device_data"], dict):
raise errors.SynapseError(
400,
"device_data must be an object",
errcode=errors.Codes.INVALID_PARAM,
)

device_id = await self.device_handler.store_dehydrated_device(
requester.user.to_string(),
submission["device_data"],
submission.get("initial_device_display_name", None),
)
return 200, {"device_id": device_id}


class ClaimDehydratedDeviceServlet(RestServlet):
"""Claim 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", releases=()
)

def __init__(self, hs):
super().__init__()
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)

submission = parse_json_object_from_request(request)

if "device_id" not in submission:
raise errors.SynapseError(
400, "device_id missing", errcode=errors.Codes.MISSING_PARAM,
)
elif not isinstance(submission["device_id"], str):
raise errors.SynapseError(
400, "device_id must be a string", errcode=errors.Codes.INVALID_PARAM,
)

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)
37 changes: 22 additions & 15 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 @@ -75,23 +77,28 @@ async def on_POST(self, request, device_id):
body = parse_json_object_from_request(request)

if device_id is not None:
# passing the device_id here is deprecated; however, we allow it
# for now for compatibility with older clients.
# Providing the device_id should only be done for setting keys
# for dehydrated devices; however, we allow it for any device for
# compatibility with older clients.
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 = await self.device_handler.get_dehydrated_device(
user_id
)
if dehydrated_device is not None and device_id != dehydrated_device[0]:
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

0 comments on commit 4cb44a1

Please sign in to comment.