-
Notifications
You must be signed in to change notification settings - Fork 379
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] MSC3814: Dehydrated devices with SSSS #3814
base: main
Are you sure you want to change the base?
Changes from 14 commits
80243a4
ed2c5eb
703281e
0a149c5
a4e87a6
3827bc0
12acd43
f756db3
6223db4
e3c9ac8
7f24f0d
f85c18d
4954c27
087154a
e7c8266
cf5ae99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,392 @@ | ||||||||||||||||||||||||||
# MSC3814: Dehydrated Devices with [SSSS] | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
[MSC2697] introduces device | ||||||||||||||||||||||||||
dehydration -- a method for creating a device that can be stored in a user's | ||||||||||||||||||||||||||
account and receive [Megolm] sessions. In this way, if a user has no other | ||||||||||||||||||||||||||
devices logged in, they can rehydrate the device on the next login and retrieve | ||||||||||||||||||||||||||
the [Megolm] sessions. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
However, the approach presented in that MSC has some downsides, making it | ||||||||||||||||||||||||||
tricky to implement in some clients, and presenting some UX difficulties. For | ||||||||||||||||||||||||||
example, it requires that the device rehydration be done before any other API | ||||||||||||||||||||||||||
calls are made (in particular `/sync`), which may conflict with clients that | ||||||||||||||||||||||||||
currently assume that `/sync` can be called immediately after logging in. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
In addition, the user is required to enter a key or passphrase to create a | ||||||||||||||||||||||||||
dehydrated device. In practice, this is usually the same as the [SSSS] | ||||||||||||||||||||||||||
key/passphrase, which means that the user loses the advantage of verifying | ||||||||||||||||||||||||||
their other devices via emoji or QR code: either they will still be required to | ||||||||||||||||||||||||||
enter their [SSSS] key/passphrase (or a separate one for device dehydration), or | ||||||||||||||||||||||||||
else that client will not be able to dehydrate a device. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
This proposal introduces another way to use the dehydrated device that solves | ||||||||||||||||||||||||||
these problems by storing the dehydration key in [SSSS], and by not changing the | ||||||||||||||||||||||||||
client's device ID. Rather than changing its device ID when it rehydrates the | ||||||||||||||||||||||||||
device, it will keep its device ID and upload its own device keys. The client | ||||||||||||||||||||||||||
will separately rehydrate the device, fetch its to-device messages, and decrypt | ||||||||||||||||||||||||||
them to retrieve the Megolm sessions. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Proposal | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
### Dehydrating a device | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The dehydration process is similar as in [MSC2697]. One important change is that | ||||||||||||||||||||||||||
the dehydrated device, the public device keys, and one-time keys are all | ||||||||||||||||||||||||||
uploaded in the same request. This change should prevent the creation of | ||||||||||||||||||||||||||
dehydrated devices which do not support end-to-end encryption. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
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. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The client *must* use the public [Curve25519] [identity key] of the device, | ||||||||||||||||||||||||||
encoded as unpadded Base64, as the device ID. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The `device_keys`, `one_time_keys`, and `fallback_keys` fields use the same | ||||||||||||||||||||||||||
structure as for the [`/keys/upload`] request. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`PUT /dehydrated_device` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```jsonc | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"device_id": "dehydrated_device_id", | ||||||||||||||||||||||||||
"device_data": { | ||||||||||||||||||||||||||
"algorithm": "m.dehydration.v1.olm" | ||||||||||||||||||||||||||
"other_fields": "other_values" | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
"initial_device_display_name": "foo bar", // optional | ||||||||||||||||||||||||||
"device_keys": { | ||||||||||||||||||||||||||
uhoreg marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
"user_id": "<user_id>", | ||||||||||||||||||||||||||
"device_id": "<device_id>", | ||||||||||||||||||||||||||
"valid_until_ts": <millisecond_timestamp>, | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This property isn't defined anywhere |
||||||||||||||||||||||||||
"algorithms": [ | ||||||||||||||||||||||||||
"m.olm.curve25519-aes-sha2", | ||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||
"keys": { | ||||||||||||||||||||||||||
"<algorithm>:<device_id>": "<key_base64>", | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
"signatures": { | ||||||||||||||||||||||||||
"<user_id>": { | ||||||||||||||||||||||||||
"<algorithm>:<device_id>": "<signature_base64>" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
"fallback_keys": { | ||||||||||||||||||||||||||
"<algorithm>:<device_id>": "<key_base64>", | ||||||||||||||||||||||||||
"signed_<algorithm>:<device_id>": { | ||||||||||||||||||||||||||
"fallback": true, | ||||||||||||||||||||||||||
"key": "<key_base64>", | ||||||||||||||||||||||||||
"signatures": { | ||||||||||||||||||||||||||
"<user_id>": { | ||||||||||||||||||||||||||
"<algorithm>:<device_id>": "<key_base64>" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
"one_time_keys": { | ||||||||||||||||||||||||||
"<algorithm>:<key_id>": "<key_base64>" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Result: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"device_id": "dehydrated device's ID" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
### Rehydrating a device | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
To rehydrate a device, a client first 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 an error code of | ||||||||||||||||||||||||||
`M_NOT_FOUND`, HTTP code 404. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
If the client is able to decrypt the data and wants to use the dehydrated | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can say something like: the server is allowed to discard any non- |
||||||||||||||||||||||||||
device, the client retrieves the to-device messages sent to the dehydrated | ||||||||||||||||||||||||||
device by calling `POST /dehydrated_device/{device_id}/events`, where | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why include a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that the device ID is redundant. Though since this MSC has been written a new use-case for this endpoint has been found. In the sliding sync world we have split out the fetching of to-device events into a separate sync loop. Namely one of the biggest problems of the existing To-device events are one of those things that are not directly related to the things that a client will want to display in a room or room list, so putting it into a separate sync loop allows the main loop to quickly send updates while to-device moves along in the background. More info here: matrix-org/matrix-rust-sdk#1928 I think that old sync could handle such a split as well, so I would suggest here to rename the endpoint to become There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds quite racy to me -- how does the server know that one dehydrated device is claimed? How would the client know to make a new one instead of claim the old one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's OK for multiple clients to rehydrate the same device (unlike in the previous proposal), because it never becomes a real device. So the server can just wait until some client fetches all the events before dropping the device.
Making a new device and rehydrating an old one are two different use cases. Rehydration happens after you log in, and you're setting up encryption and trying to get keys. It only happens once in the device's lifetime. Creating a new dehydrated device would happen after you've already set up your encryption and already attempted to rehydrate a device. |
||||||||||||||||||||||||||
`{device_id}` is the ID of the dehydrated device. Since there may be many | ||||||||||||||||||||||||||
uhoreg marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
messages, the response can be sent in batches: the response must include a | ||||||||||||||||||||||||||
`next_batch` parameter, which can be used in a subsequent call to `POST | ||||||||||||||||||||||||||
/dehydrated_device/{device_id}/events` to obtain the next batch. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
POST /dehydrated_device/{device_id}/events | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a POST and not a GET like /sync and /messages? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, the rationale was because the call has side-effects (deleting the device). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit weird that it doesn't follow the pattern of /messages, /events or /sync imo. I'll try implementing it as a GET without the device deletion first and see how that works out, I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A GET endpoint with side-effects seems like a big no-no to me. Everyone expects a GET request to have approximately zero side-effects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, but we're also proposing removing the side-effects? SGTM in that case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, the current implementation no longer automatically deletes the device on the server side, but relies on the client to delete/create a new device. So we're going to try to make this a GET. |
||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"next_batch": "token from previous call" // (optional) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Response: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```jsonc | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"events": [ | ||||||||||||||||||||||||||
// array of to-device messages, in the same format as in | ||||||||||||||||||||||||||
// https://spec.matrix.org/unstable/client-server-api/#extensions-to-sync | ||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||
"next_batch": "token to obtain next events" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Once a client calls `POST /dehydrated_device/{device_id}/events` with a | ||||||||||||||||||||||||||
`next_batch` token, unlike the `/sync` endpoint, the server should *not* delete | ||||||||||||||||||||||||||
any to-device messages delivered in previous batches. This should prevent the | ||||||||||||||||||||||||||
loss of messages in case the device performing the rehydration gets deleted. In | ||||||||||||||||||||||||||
the case the rehydration process gets aborted, another device will be able to | ||||||||||||||||||||||||||
restart the process. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
For the last batch of messages, the server will still send a | ||||||||||||||||||||||||||
`next_batch` token, and return an empty `events` array when called with that | ||||||||||||||||||||||||||
Comment on lines
+173
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not what https://spec.matrix.org/v1.9/appendices/#pagination says should happen. I'd like to see this changed before this stabilises. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original reason why this endpoint used an empty |
||||||||||||||||||||||||||
token, this signals to the client that it has received all to-device events and | ||||||||||||||||||||||||||
it can delete the dehydrated device and create a new one. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
If the given `device_id` is not the dehydrated device ID, the server responds | ||||||||||||||||||||||||||
with an error code of `M_FORBIDDEN`, HTTP code 403. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
### Deleting a dehydrated device | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably specify what happens when we use |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
A dehydrated device will get replaced whenever a new device gets uploaded using | ||||||||||||||||||||||||||
the `PUT /dehydrated_device`, this makes a `DELETE /dehydrated_device` | ||||||||||||||||||||||||||
unnecessary, though for completeness sake and to give client authors to get back | ||||||||||||||||||||||||||
to a state where no dehydrated device exists for a given user we will introduce | ||||||||||||||||||||||||||
one. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`DELETE /dehydrated_device` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Response: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"device_id": "dehydrated device's ID" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
### Device Dehydration Format | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
TODO: define a format. Unlike MSC2679, we don't need to worry about the | ||||||||||||||||||||||||||
dehydrated device being used as a normal device, so we can omit some | ||||||||||||||||||||||||||
information. So we should be able to get by with defining a fairly simple | ||||||||||||||||||||||||||
standard format, probably just the concatenation of the private device keys and | ||||||||||||||||||||||||||
the private one-time keys. This will come at the expense of implementations | ||||||||||||||||||||||||||
such as libolm needing to implement extra functions to support dehydration, but | ||||||||||||||||||||||||||
will have the advantage that we don't need to figure out a format that will fit | ||||||||||||||||||||||||||
into every possible implementation's idiosyncrasies. The format will be | ||||||||||||||||||||||||||
encrypted, which leads to ... | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```text | ||||||||||||||||||||||||||
┌───────────────────────────────────────────────────────────┐ | ||||||||||||||||||||||||||
│ Pickle │ | ||||||||||||||||||||||||||
├───────────────────────────────────────────────────────────┤ | ||||||||||||||||||||||||||
│Name │ Type │ Size (bytes) │ | ||||||||||||||||||||||||||
├────────────────────────┼───────────────┼──────────────────┤ | ||||||||||||||||||||||||||
│Version │ u32 │ 4 │ | ||||||||||||||||||||||||||
│Ed25519 key pair │ KeyPair │ 64 │ | ||||||||||||||||||||||||||
│Curve25519 key pair │ KeyPair │ 64 │ | ||||||||||||||||||||||||||
│Number of one-time keys │ u32 │ 4 │ | ||||||||||||||||||||||||||
│One-time keys │ [OneTimeKey] │ N * 69 │ | ||||||||||||||||||||||||||
│Fallback keys │ FallbackKeys │ 2 * 69 │ | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we only need one fallback key. Normal clients store two fallback keys because they upload a new fallback key after the current fallback key is used. But with dehydrated devices, we will never upload a new fallback. |
||||||||||||||||||||||||||
│Next key ID │ u32 │ 4 │ | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise, I don't think we need this because we won't upload new OTKs after we upload the dehydrated device. It also makes assumptions about the structure of the OTK IDs, which may not be true for all olm implementations. |
||||||||||||||||||||||||||
└────────────────────────┴───────────────┴──────────────────┘ | ||||||||||||||||||||||||||
┌───────────────────────────────────────────────────────────┐ | ||||||||||||||||||||||||||
│ KeyPair │ | ||||||||||||||||||||||||||
├────────────────────────┬───────────────┬──────────────────┤ | ||||||||||||||||||||||||||
│Name │ Type │ Size (bytes) │ | ||||||||||||||||||||||||||
├────────────────────────┼───────────────┼──────────────────┤ | ||||||||||||||||||||||||||
│Public key │ [u8; 32] │ 32 │ | ||||||||||||||||||||||||||
│Private key │ [u8; 32] │ 32 │ | ||||||||||||||||||||||||||
└────────────────────────┴───────────────┴──────────────────┘ | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
┌───────────────────────────────────────────────────────────┐ | ||||||||||||||||||||||||||
│ OneTimeKey │ | ||||||||||||||||||||||||||
├────────────────────────┬───────────────┬──────────────────┤ | ||||||||||||||||||||||||||
│Name │ Type │ Size (bytes) │ | ||||||||||||||||||||||||||
├────────────────────────┼───────────────┼──────────────────┤ | ||||||||||||||||||||||||||
│Key ID │ u32 │ 4 │ | ||||||||||||||||||||||||||
│Is published │ u8 │ 1 │ | ||||||||||||||||||||||||||
Comment on lines
+239
to
+240
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need these, because the key ID doesn't get used when decrypting events, and any OTKs that are included in the dehydrated device will surely have been published -- any unpublished OTKs should just be omitted. |
||||||||||||||||||||||||||
│Curve 25519 key pair │ KeyPair │ 69 │ | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
└────────────────────────┴───────────────┴──────────────────┘ | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
┌───────────────────────────────────────────────────────────┐ | ||||||||||||||||||||||||||
│ FallbackKeys │ | ||||||||||||||||||||||||||
├────────────────────────┬───────────────┬──────────────────┤ | ||||||||||||||||||||||||||
│Name │ Type │ Size (bytes) │ | ||||||||||||||||||||||||||
├────────────────────────┼───────────────┼──────────────────┤ | ||||||||||||||||||||||||||
│Number of fallback keys │ u8 │ 1 │ | ||||||||||||||||||||||||||
│Fallback-key │ OneTimeKey │ 69 │ | ||||||||||||||||||||||||||
│Previous fallback-key │ OneTImeKey │ 69 │ | ||||||||||||||||||||||||||
└────────────────────────┴───────────────┴──────────────────┘ | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
TODO: Explain why we must ignore public keys when decoding them and why they are | ||||||||||||||||||||||||||
included in the first place. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
When decoding, clients *must* ignore the public keys and instead derive the | ||||||||||||||||||||||||||
public key from the private one. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#### Encryption key | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
TODO: Explain why the double derivation is necessary. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The encryption key used for the dehydrated device will be randomly generated | ||||||||||||||||||||||||||
and stored/shared via SSSS using the name `m.dehydrated_device`. | ||||||||||||||||||||||||||
uhoreg marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if I'm reading the iOS implementation correctly, the key is encoded with unpadded base64 (as is done with the other keys in secret storage) |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The randomly generated encryption key *must* be expanded using the HMAC-based | ||||||||||||||||||||||||||
Key Derivation function defined in [RFC5869]. | ||||||||||||||||||||||||||
Comment on lines
+268
to
+269
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```math | ||||||||||||||||||||||||||
\begin{aligned} | ||||||||||||||||||||||||||
DEVICE\_KEY | ||||||||||||||||||||||||||
&= \text{HKDF} \left(\text{``Device ID``}, RANDOM\_KEY, \text{``dehydrated-device-pickle-key"}, 32\right) | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also the quotes around |
||||||||||||||||||||||||||
\end{aligned} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
Comment on lines
+271
to
+276
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The `device_key` is then further expanded into a AES256 key, HMAC key and | ||||||||||||||||||||||||||
initialization vector. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```math | ||||||||||||||||||||||||||
\begin{aligned} | ||||||||||||||||||||||||||
AES\_KEY \parallel HMAC\_KEY \parallel AES\_IV | ||||||||||||||||||||||||||
&= \text{HKDF}\left(0,DEVICE\_KEY,\text{``Pickle"},80\right) | ||||||||||||||||||||||||||
Comment on lines
+284
to
+285
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the IV not random here? Is there a reason why this encryption needs to be deterministic? I don't think there is. But even if so, why not use a well known deterministic encryption mode like SIV? Otherwise this looks pretty sketchy. In practice it's probably not going to lead to an exploit, but it should probably get flagged in a security audit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I think I see what's going on. Based on this comment https://github.com/matrix-org/matrix-rust-sdk/blob/794b11a0cecd2fab8e43931639eba5212fb89921/crates/matrix-sdk-crypto/src/dehydrated_devices.rs#L347 you're using the libolm pickle encryption scheme? But given the warning in that Rust comment, it sounds like the potential for IV reuse here is a real problem. If an application fails to generate a new random key when it creates a new dehydrated device, then you're going to reuse the IV. So I guess this just brings me back to my question below: Why not just use the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incidentally, I don't think we define the HKDF parameter order anywhere. Not in this MSC and not in the spec. The only place that defines it is the Olm specification. We should define it in the MSC and then reflect in the spec as well. |
||||||||||||||||||||||||||
\end{aligned} | ||||||||||||||||||||||||||
Comment on lines
+283
to
+286
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The plain-text is encrypted with [AES-256] in [CBC] mode with [PKCS#7] padding, | ||||||||||||||||||||||||||
using the key $`AES\_KEY`$ and the IV $`AES\_IV`$ to give the cipher-text. | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Then the cipher-text are passed through [HMAC-SHA-256]. The first 8 bytes of the | ||||||||||||||||||||||||||
MAC are appended to the cipher-text. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The cipher-text, including the appended MAC tag, are encoded using unpadded | ||||||||||||||||||||||||||
Base64 to give the device pickle. | ||||||||||||||||||||||||||
Comment on lines
+271
to
+296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this key derivation so different from the existing symmetric scheme for encrypting secrets in secret storage? I'm looking at https://spec.matrix.org/v1.10/client-server-api/#msecret_storagev1aes-hmac-sha2-1 If the existing If it is good enough, then why do something randomly different here? IMO if there's going to be a new scheme for encrypting secrets in secret storage, then that should be a new MSC all on its own. Otherwise it feels like meaningless duplication of effort, and more chance for implementations to make a stupid mistake. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you have noticed, this is a scheme already used in the stack, it's used by Olm/Megolm. The Not that I'm disagreeing with you on your other points, but the scheme isn't randomly different. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we name the RFC we are basing our Base64 encoding on? |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The device pickle is then inserted into the `device_pickle` field of the | ||||||||||||||||||||||||||
`device_data` JSON message. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
"device_data": { | ||||||||||||||||||||||||||
"algorithm": "m.dehydration.v1.olm", | ||||||||||||||||||||||||||
"device_pickle": "encrypted dehydrated device" | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#### Test vectors | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Device pickle: | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
Gc0elC7k7NISzWW/C2UIuzRMDSHzzRLfM3lMnJHMLMcuyLtZHljhV/YvIctIlepxevznEcwBc40Q0CtS3k5SI9gGyN7G+95hnQan0rKe64a1Vx1Vx4Ky8i+m1y9JVT++WcQ54CGhMuCGoN2O1xEQb+4fM+UVS/bLNJ4Pzzqa1ilzCrs4SCTz70eriShvzt7y1cn2A6ABNhK4aXnLB8gK9HuMLyctyX5ikvIjkAIAdVr1EI1azetZDQ | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Potential issues | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The same issues as in | ||||||||||||||||||||||||||
[MSC2697](https://github.com/matrix-org/matrix-doc/pull/2697) are present for | ||||||||||||||||||||||||||
this proposal. For completeness, they are repeated here: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
### 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). | ||||||||||||||||||||||||||
uhoreg marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
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. | ||||||||||||||||||||||||||
Comment on lines
+336
to
+340
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we doing this [replacing the dehydrated device periodically] or not? It seems like both have serious downsides. If we do replace it, we have a very racy operation that is certain to cause UTDs in practice. If we don't replace it, then we'll end up with no remaining OTKs at all, and an incredibly long list of to-device messages all of which have to be downloaded and decrypted by any new clients. |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Alternatively, the client could perform a `/sync` for the dehydrated device, | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this sill works with v2? can we still sync on the dehydrated device? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't sync as a different device in this proposal. You can fetch the events for that device, but this proposal implicitly deletes the device in that case, which means you can't keep the device alive after that. So imo your only option is to replace it (which is somewhat easy to do, but you might need to authenticate the new signature upload/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. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Alternatives | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
As mentioned above, | ||||||||||||||||||||||||||
[MSC2697](https://github.com/matrix-org/matrix-doc/pull/2697) tries to solve | ||||||||||||||||||||||||||
the same problem in a similar manner, but has several disadvantages that are | ||||||||||||||||||||||||||
fixed in this proposal. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Rather than keep the name "dehydrated device", we could change the name to | ||||||||||||||||||||||||||
something like "shrivelled sessions", so that the full expansion of this MSC | ||||||||||||||||||||||||||
title would be "Shrivelled Sessions with Secure Secret Storage and Sharing", or | ||||||||||||||||||||||||||
SSSSSS. However, despite the alliterative property, the term "shrivelled | ||||||||||||||||||||||||||
sessions" is less pleasant, and "dehydrated device" is already commonly used to | ||||||||||||||||||||||||||
refer to this feature. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The alternatives discussed in MSC2697 are also alternatives here. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Security considerations | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This section should contain a discussion of malicious dehydrated devices injected by the server. The MSC in its current form is almost purely focused on the mechanics of creation and rehydration of dehydrated devices. There is very little discussion of how these devices should be treated by message senders. And yet, these are not ordinary devices, as evidenced by the fact that we are proposing special UI/UX for them in the section concerning the The long-term plan is to move to a model where a user's devices must be signed by the user's cryptographic identity in order to be considered valid (see MSC4153) . Given that context, and the fact that dehydrated devices are a completely new feature, I strongly recommend that this MSC should require that a dehydrated device MUST be signed by a pinned (TOFU-trusted) user identity in order to be considered valid. If the dehydrated device is not signed, or is signed by a user identity which is not the one that is currently pinned by the client, the dehydrated device MUST be ignored by senders as if it it does no exist. That is, clients MUST NOT send any to-device messages to such a device nor accept any to-device messages from it. |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
A similar security consideration to the one in MSC2697 also applies to this | ||||||||||||||||||||||||||
proposal: if SSSS is encrypted using a weak passphrase or key, an attacker | ||||||||||||||||||||||||||
could access it and rehydrate the device to 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.msc3814.v1/dehydrated_device`, and the | ||||||||||||||||||||||||||
`/dehydrated_device/{device_id}/events` endpoint will be reached at | ||||||||||||||||||||||||||
`/unstable/org.matrix.msc3814.v1/dehydrated_device/{device_id}/events`. The | ||||||||||||||||||||||||||
dehydration algorithm `m.dehydration.v1.olm` will be called | ||||||||||||||||||||||||||
`org.matrix.msc3814.v1.olm`. The SSSS name for the dehydration key will be | ||||||||||||||||||||||||||
`org.matrix.msc3814` instead of `m.dehydrated_device`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Client implementation: https://gitlab.com/famedly/company/frontend/famedlysdk/-/merge_requests/1111 Server implementation: matrix-org/synapse#13581 Both not merged yet and notably missing is the dehydrated device format. |
||||||||||||||||||||||||||
## Dependencies | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
None | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
[RFC5869]: https://datatracker.ietf.org/doc/html/rfc5869 | ||||||||||||||||||||||||||
[AES-256]: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf | ||||||||||||||||||||||||||
[CBC]: http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf | ||||||||||||||||||||||||||
[PKCS#7]: https://tools.ietf.org/html/rfc2315 | ||||||||||||||||||||||||||
[Curve25519]: http://cr.yp.to/ecdh.html | ||||||||||||||||||||||||||
[identity key]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md#initial-setup | ||||||||||||||||||||||||||
[Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md | ||||||||||||||||||||||||||
[SSSS]: https://spec.matrix.org/v1.7/client-server-api/#storage | ||||||||||||||||||||||||||
[MSC2697]: https://github.com/matrix-org/matrix-doc/pull/2697 | ||||||||||||||||||||||||||
[`/keys/upload`]: https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysupload | ||||||||||||||||||||||||||
[device keys]: https://spec.matrix.org/v1.7/client-server-api/#device-keys | ||||||||||||||||||||||||||
[HMAC-SHA-256]: https://datatracker.ietf.org/doc/html/rfc2104 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the device ID ends up in a path parameter, we should probably make this URL-safe Base64 to avoid issues with clients failing to URL-encode the ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was previously considered, but was rejected as we would end up with two different representations of the same data (as device ID and as the key), which would be confusing.