Skip to content
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

[WIP] MSC2697: Device dehydration #2697

Closed
wants to merge 9 commits into from
191 changes: 191 additions & 0 deletions proposals/2697-device-dehydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# MSC2697: Device Dehydration

End-to-end encryption in Matrix relies on the sending device being able to send
megolm sessions to the recipients' devices. When a
user logs into a new device, they can obtain the megolm sessions using key
backup or key sharing if another of their devices had previously received the
session. However, when a user has no logged-in devices when a message is sent,
they are unable to receive incoming megolm sessions.

One solution to this is have a dehydrated device stored (encrypted)
server-side, which may be rehydrated and used when the user creates a new
login rather than creating a new device from scratch. The new login will
receive any to-device messages that were sent to the dehydrated device.

## Proposal

### Dehydrating a device

To upload a new dehydrated device, a client will use `PUT /dehydrated_device`.
Each user has at most one dehydrated device; uploading a new dehydrated device
will remove any previously-set dehydrated device.

`PUT /dehydrated_device`

```json
{
"device_data": {
"algorithm": "m.dehydration.v1.olm"
"other_fields": "other_values"
},
"initial_device_display_name": "foo bar",
}
```
Comment on lines +25 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably:

  • device_data andalgorithm are mandatory
  • other fields can be freely included in device_data
  • initial device display name is optional (mirroring /login)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct


Result:

```json
{
"device_id": "dehydrated device's ID"
}
```

After the dehydrated device is uploaded, the client will upload the encryption
keys using `POST /keys/upload/{device_id}`, where the `device_id` parameter is
the device ID given in the response to `PUT /dehydrated_device`. The request
and response formats for `POST /keys/upload/{device_id}` are the same as those
for `POST /keys/upload` with the exception of the addition of the `device_id`
path parameter.

Note: Synapse already supports `POST /keys/upload/{device_id}` as this was used
in some old clients. However, synapse requires that the given device ID
matches the device ID of the client that made the call. So this will be
changed to allow uploading keys for the dehydrated device.

### Rehydrating a device

To rehydrate a device, a client first logs in as normal and then calls `GET
/dehydrated_device` to see if a dehydrated device is available. If a device is
available, the server will respond with the dehydrated device's device ID and
the dehydrated device data.

`GET /dehydrated_device`

Response:

```json
{
"device_id": "dehydrated device's ID",
"device_data": {
"algorithm": "m.dehydration.v1.olm",
"other_fields": "other_values"
}
}
```

If no dehydrated device is available, the server responds with a 404.
uhoreg marked this conversation as resolved.
Show resolved Hide resolved

If the client is able to decrypt the data and wants to use the dehydrated
device, the client calls `POST /dehydrated_device/claim` to tell the server
that it wishes to do so. The request includes the device ID of the dehydrated
device to ensure that it is still available.

If the requested device is still the current dehydrated device and has not
already been claimed by another client, the server responds with a successful
response. The server also changes the device ID associated with the client's
access token to the device ID of the dehydrated device, and will change the
device display name for the dehydrated device to the display name given when
the client logged in.

`POST /dehydrated_device/claim`

```json
{
"device_id": "dehydrated device's ID"
}
```

Response:

```json
{
"success": true
}
```

If the requested device ID does not belong to the user's current dehydrated
device or the dehydrated device has already been claimed, the server responds
with a 404.

Clients should not call any other endpoints before rehydrating a device. In
particular, if a client calls `/sync` while rehydrating, the client should not
expect the `/sync` to return sensible information. For example, it could
contain a mix of to-device messages sent to the old device ID and the new
device ID.

### Device dehydration format
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to break out full examples on this? Creating types for this isn't the easiest thing at the moment (hard to see what it replies with):

export interface IDehydratedDevice {
    device_id: string;
    device_data: {
        algorithm: string;

        // TODO: Fill in from MSC2697
        // https://github.com/matrix-org/matrix-doc/pull/2697
        [key: string]: any;
    }
}


The `device_data` property is an object that has an `algorithm` field
indicating what other fields are present.

#### `m.dehydration.v1.olm.libolm_pickle`

- `passphrase`: Optional. Indicates how to generate the decryption key from a
passphrase. It is in the same format with Secure Secret Storage.
- `account`: Required. FIXME: libolm's pickle format
Comment on lines +121 to +125
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ideal that we're tying this to a particular implementation's format, not least because, as noted over at matrix-org/olm#53, it's not entirely obvious that libolm's pickles are the most sensible format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Vodozemac is also defining its own JSON-based pickle format.

libolm's pickle format is very much tied to some implementation details (e.g. otk IDs being a counter), so may not be suitable for other implementations. On the other hand, a more generic format may not be usable by libolm. We may end up with a situation where we'll end up with multiple pickle formats for dehydration (e.g. m.dehydration.v1.olm.libolm_pickle, m.dehydration.v1.olm.vodozemac_pickle), and you'll only be able to rehydrate if your implementation supports that format. Which is ... not ideal, but may be unavoidable unless we also want to end up dictating certain things about implementation details.


## Potential issues

### One-time key exhaustion

The dehydrated device may run out of one-time keys, since it is not backed by
an active client that can replenish them. Once a device has run out of
one-time keys, no new olm sessions can be established with it, which means that
devices that have not already shared megolm keys with the dehydrated device
will not be able to share megolm keys. This issue is not unique to dehydrated
devices; this also occurs when devices are offline for an extended period of
time.

This may be addressed by using fallback keys as described in
[MSC2732](https://github.com/matrix-org/matrix-doc/pull/2732).

To reduce the chances of one-time key exhaustion, if the user has an active
client, it can periodically replace the dehydrated device with a new dehydrated
device with new one-time keys. If a client does this, then it runs the risk of
losing any megolm keys that were sent to the dehydrated device, but the client
would likely have received those megolm keys itself.

Alternatively, the client could perform a `/sync` for the dehydrated device,
dehydrate the olm sessions, and upload new one-time keys. By doing this
instead of overwriting the dehydrated device, the device can receive megolm
keys from more devices. However, this would require additional server-side
changes above what this proposal provides, so this approach is not possible for
the moment.

### Accumulated to-device messages

If a dehydrated device is not rehydrated for a long time, then it may
accumulate many to-device messages from other clients sending it megolm
sessions. This may result in a slower initial sync when the device eventually
does get rehydrated, due to the number of messages that it will retrieve.
Again, this can be addressed by periodically replacing the dehydrated device,
or by performing a `/sync` for the dehydrated device and updating it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is I think some side effects to expect with verifications and device list management.
A dehydrated device will appear as a regular device in keys/query right?

  1. Should we display them as a regular device in the device list manager? Wouldn't it look suspicious for user with only one login to see 2 devices?

  2. Also what happen when you request a verification to such devices? Does it impacts the self verification process (where we check for existing devices?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client could tell which device is the dehydrated device by calling GET /dehydrated_device and comparing device IDs. Alternatively, (and maybe better) we could make the server throw a flag in the unsigned portion of the device key object, maybe a "dehydrated": true.

Either way, it would be up to the client to handle the issues that you listed, e.g. by displaying them differently from regular devices, omitting them from verifications, etc.

## Alternatives

Rather than uploading a dehydrated device to the server, we could instead have
the sender resend the megolm session in the case where a user had no active
devices at the time that a message was sent. However this does not solve the
issue for users who happen to never be logged in at the same time. But this could
be done in addition to the solution proposed here.

The sender could also send the megolm session to a the user using a public key
using some per-user mechanism. This would require changes to both the sender
and receiver (whereas this proposal only requires changes to the receiver), and
would require developing a system by which the sender could determine whether
the public key may be trusted (whereas this proposal the existing cross-signing
mechanism).

## Security considerations

If the dehydrated device is encrypted using a weak password or key, an attacker
could access it and read the user's encrypted messages.

## Unstable prefix

While this MSC is in development, the `/dehydrated_device` endpoints will be
reached at `/unstable/org.matrix.msc2697.v2/dehydrated_device`, and the
`/dehydrated_device/claim` endpoint will be reached at
`/unstable/org.matrix.msc2697.v2/dehydrated_device`. The dehydration algorithm
`m.dehydration.v1.olm.libolm_pickle` will be called
`org.matrix.msc2697.v1.olm.libolm_pickle`.