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] MSC3814: Dehydrated devices with SSSS #3814

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
80243a4
Initial proposal for dehydrated devices with SSSS
uhoreg May 12, 2022
ed2c5eb
use MSC number
uhoreg May 12, 2022
703281e
wording improvements and clarifications
uhoreg Sep 5, 2022
0a149c5
Uploading a dehydrated device now uploads the public keys as well
poljar Aug 9, 2023
a4e87a6
Make the next_batch token non-optional in the response
poljar Aug 9, 2023
3827bc0
Let's not delete to-device events when a client receives them
poljar Aug 9, 2023
12acd43
Introduce the DELETE endpoint
poljar Aug 9, 2023
f756db3
Attempt to define the dehydration format
poljar Aug 10, 2023
6223db4
Don't use operatorname, try to unwedge the Latex
poljar Aug 10, 2023
e3c9ac8
More Latex tweaks
poljar Aug 10, 2023
7f24f0d
Remove the bytes unit from every single row, put it in the header
poljar Sep 5, 2023
f85c18d
Attempt to fix the math rendering
poljar Sep 5, 2023
4954c27
Align the table headers for the pickle format
poljar Sep 5, 2023
087154a
Fix JSON example
uhoreg Feb 8, 2024
e7c8266
link to fallback key spec
uhoreg Feb 12, 2024
cf5ae99
add dehydrated flag
uhoreg Feb 23, 2024
d751d33
Apply suggestions from code review
uhoreg Nov 19, 2024
11149e4
define new format and add more security notes
uhoreg Nov 22, 2024
1500897
suggest dropping to-device messages sent from a dehydrated device
uhoreg Dec 16, 2024
5742c52
link to the base64 format that we use
uhoreg Dec 16, 2024
a58288a
we need the ed25519 key after all
uhoreg Dec 16, 2024
21a3d67
add note about URL-safe base64
uhoreg Dec 16, 2024
6be9078
update dehydration format
uhoreg Dec 28, 2024
ec17903
fix typo
uhoreg Jan 10, 2025
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
390 changes: 390 additions & 0 deletions proposals/3814-dehydrated-devices-with-ssss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
# 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 dehydrated device *must* be cross-signed and have a fallback key.

The client *must* use the public [Curve25519] [identity key] of the device,
encoded as unpadded Base64, as the device ID.
uhoreg marked this conversation as resolved.
Show resolved Hide resolved

The `device_keys`, `one_time_keys`, and `fallback_keys` fields use the same
structure as for the [`/keys/upload`] request.

We add a new optional property to the device keys: `dehydrated`, which is set to
`true` for dehydrated devices. Defaults to `false`. Clients that support
dehydrated devices *must not* encrypt to devices marked as being a dehydrated
device if they are not cross-signed. Clients should also drop any to-device
messages from a device marked as being a dehydrated device, since dehydrated
device should not be sending messages. Clients can use also this flag to for
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
device should not be sending messages. Clients can use also this flag to for
device should not be sending messages. Clients can use also this flag for

Copy link
Member

@andybalaam andybalaam Jan 10, 2025

Choose a reason for hiding this comment

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

In the case of encrypted messages, we can identify the sending device using the sender key, or MSC4147 device info. 2 questions:

  1. What about unencrypted messages? Even if the event content contains a device id, we can't trust that a malicious homeserver didn't just fake it. Should we just allow all unencrypted to-device messages?
  2. What if the sender key is not found, maybe because the device is logged out (and there is no MSC4147 device info)? Should we assume the device is not dehydrated and process it as normal? This gives a way to work around the messages being dropped, but it's hard to see what else we can do in this case.

Copy link
Member

Choose a reason for hiding this comment

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

cc @dkasak

other purposes, such as:

- Display dehydrated devices differently from normal devices, to avoid confusing
from users who do not expect to see another device.
- Don't send key forwarding requests to the dehydrated device, since it will
not respond to them.
- Don't send room keys to the dehydrated device if the user has a sufficient
number of other devices, with the assumption that if the user logs in to a
new device, they can get the room keys from one of their other devices and/or
from key backup. This will reduce the chances that the dehydrated device
will run out of one-time keys, and reduce the number of events that the
dehydrated device will need to decrypt.

`PUT /dehydrated_device`

```jsonc
{
"device_id": "dehydrated_device_id",
"device_data": {
"algorithm": "m.dehydration.v2.olm"
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
"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>,
Copy link
Member Author

Choose a reason for hiding this comment

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

This property isn't defined anywhere

"dehydrated": true,
"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.v2",
"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
Copy link
Member Author

Choose a reason for hiding this comment

The 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-m.room.encrypted to-device message that it receives for the dehydrated device. There's no point in keeping key requests sent to the dehydrated device because it won't send anything back.

device, the client retrieves the to-device messages sent to the dehydrated
device by calling `POST /dehydrated_device/{device_id}/events`, where
Copy link
Member

Choose a reason for hiding this comment

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

Why include a device_id if you can only have a single dehydrated device? It has implications that you can provide more than one (and causes additional error checking of whether the provided device ID matches the dehydrated device ID).

Copy link
Contributor

Choose a reason for hiding this comment

The 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 /sync mechanism is that you get too much data all at once and the downloading and processing of that data prevents the UI from being updated.

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 /sync/to_device/{device_id} where device_id might be optional and used only in the case of a dehydrated device.

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 reason for the device_id parameter is that, while one client is fetching events, another client could create a new dehydrated device. Without the device_id parameter, the server could think that the client wants to fetch the events for the new device which, if there are any, it won't be able to decrypt since it's for a device that it doesn't know about. With the device_id parameter, the server will at least be able to say that there are no more events (since the device has been replaced by a new one).

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

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

how does the server know that one dehydrated device is claimed?

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.

How would the client know to make a new one instead of claim the old one?

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

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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).

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 +179 to +180
Copy link
Member

Choose a reason for hiding this comment

The 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.

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 original reason why this endpoint used an empty events array to signal the end was that the client using the final next_batch token would signal to the server that it could delete the to-device messages. Since we aren't doing that any more, we can make it work like the appendix says it should.

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
Copy link
Member Author

Choose a reason for hiding this comment

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

We should probably specify what happens when we use POST /delete_devices and DELETE /devices/{deviceId} on the dehydrated device. Also POST /logout/all. Presumably those would delete the dehydrated device. So maybe we don't need our own DELETE endpoint here? Though this endpoint allows you to delete the dehydrated device without knowing the device ID, so might still be useful.


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

We define the following format for storing the dehydrated device (based on the
libolm pickle format):

We store the device as a concatenation of binary values. Multi-byte numbers are
stored in big-endian format. The version is set to 0x80000000. (Setting the
high bit to 1 is to avoid confusion with the libolm pickle format for accounts,
which was used in a previous version of this MSC.)

```text
┌───────────────────────────────────────────────────────────┐
│ Pickle │
├───────────────────────────────────────────────────────────┤
│Name │ Type │ Size (bytes) │
├────────────────────────┼───────────────┼──────────────────┤
│Version │ u32 │ 4 │
│Curve25519 private key │ [u8; 32] │ 32 │
│Ed25519 private key │ [u8; 32] │ 32 │
│Number of one-time keys │ u32 │ 4 │
│One-time keys │ [OneTimeKey] │ N * 32 │
│Fallback key │ OptFallback │ 1 or 33 │
└────────────────────────┴───────────────┴──────────────────┘

┌───────────────────────────────────────────────────────────┐
│ OneTimeKey │
├────────────────────────┬───────────────┬──────────────────┤
│Name │ Type │ Size (bytes) │
├────────────────────────┼───────────────┼──────────────────┤
│Curve25519 private key │ [u8; 32] │ 32 │
└────────────────────────┴───────────────┴──────────────────┘

┌───────────────────────────────────────────────────────────┐
│ OptFallback │
├────────────────────────┬───────────────┬──────────────────┤
│Name │ Type │ Size (bytes) │
├────────────────────────┼───────────────┼──────────────────┤
│Fallback present │ boolean │ 1 │
│Fallback private key │ [u8; 32] │ 0 or 32 │
└────────────────────────┴───────────────┴──────────────────┘
```

The data is then encrypted and encoded as follows.

#### Encryption key

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
Copy link
Member Author

Choose a reason for hiding this comment

The 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)


A 96-bit (12-byte) nonce is randomly generated; each time a device is
dehydrated, a new nonce must be generated.

The plain-text is encrypted with ChaCha20-Poly1305 as defined in
[RFC8439](https://datatracker.ietf.org/doc/html/rfc8439) using the encryption
key and nonce.

The ciphertext and nonce are then encoded as [unpadded
Base64](https://spec.matrix.org/v1.12/appendices/#unpadded-base64) and inserted
into the `device_pickle` and `nonce` properties, respectively, of the
`device_data` JSON message. The `algorithm` property is set to `m.dehydration.v2`.

```json
{
"device_data": {
"algorithm": "m.dehydration.v2",
"device_pickle": "encrypted dehydrated device"
"nonce": "random nonce"
}
}
```

#### Test vectors

TODO: put a test vector here

## 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](https://spec.matrix.org/v1.9/client-server-api/#one-time-and-fallback-keys).

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 +298 to +302
Copy link
Member

Choose a reason for hiding this comment

The 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,
Copy link
Member

Choose a reason for hiding this comment

The 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?

Choose a reason for hiding this comment

The 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.

Since the device ID is used in URL path parameters, we could use URL-safe base64
to derive the device ID. However, this would result in the identity key being
represented in two similar-but-different ways (URL-safe base64 in the device ID,
and regular base64 in the device keys structure), which could lead to confusion.

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
Copy link
Member

Choose a reason for hiding this comment

The 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 dehydrated flag. The obvious concern is that dehydrated devices could end up being hidden or obscured from the user's view—even more so than ordinary devices—and therefore bear an even greater risk of malicious injection.

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is, clients MUST NOT send any to-device messages to such a device nor accept any to-device messages from it.

Dehydrated devices shouldn't be sending to-device messages, so it's probably safe to say that we should not accept any to-device messages from any devices marked as dehydrated.


### Weak SSSS passphrase/key

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.

### Display of dehydrated devices

As mentioned earlier, clients may wish to display dehydrated devices differently
from normal devices by checking the `dehydrated` flag in the device's keys.
Clients must exercise care when doing so, as this may allow a attacker to hide a
malicious device. Clients *must not* encrypt messages to a dehydrated device
that is not cross-signed. Clients should indicate the presence of the
dehydrated device, even if it is not listed along with the normal devices. For
example, a client could hide the dehydrated device from the device list, but
indicate that "The dehydrated device feature is enabled". A user can only have
one dehydrated device available at a time, so if more than one device is marked
as `dehydrated: true`, the client should display them all as normal devices.
Clients can also display a warning in such a situation.

## 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.v2` will be called
`org.matrix.msc3814.v2`. The SSSS name for the dehydration key will be
`org.matrix.msc3814` instead of `m.dehydrated_device`.

Choose a reason for hiding this comment

The 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
Loading