matrix.org/content/docs/matrix-concepts/end-to-end-encryption/_index.md

567 lines
26 KiB
Markdown

+++
title = "End-to-End Encryption implementation guide"
weight = 900
template = "docs/with_menu.html"
aliases = ["/docs/guides/end-to-end-encryption-implementation-guide", "/docs/legacy/e2e-implementation/"]
[extra]
updated = "2023-02-08T08:00:00Z"
meta_description = """
This guide is intended for authors of Matrix clients who wish to add support for
end-to-end encryption. It is highly recommended that readers be familiar with
the Matrix protocol and the use of access tokens before proceeding.
"""
+++
## Implementing End-to-End Encryption in Matrix clients
This guide is intended for authors of Matrix clients who wish to add support for
end-to-end encryption. It is highly recommended that readers be familiar with
the Matrix protocol and the use of access tokens before proceeding.
### Olm/Megolm implementations
End-to-end encryption in Matrix is based on the Olm and Megolm cryptographic ratchets. The recommended starting point for any client authors is with the [vodozemac](https://github.com/matrix-org/vodozemac/), which contains implementations of all the cryptographic methods required. [libolm](https://gitlab.matrix.org/matrix-org/olm) is also available.
### Devices
We have a particular meaning for "device". As a user, I might have several
devices (a desktop client, some web browsers, an Android device, an iPhone,
etc). When I first use a client, it should register itself as a new device. If
I log out and log in again as a different user, the client must register as a
new device. Critically, the client must create a new set of keys (see below)
for each "device".
The longevity of devices will depend on the client. In the web client, we create
a new device every single time you log in. In a mobile client, it might be
acceptable to reuse the device if a login session expires,
**provided** the user is the same. **Never** share keys between different
users.
Devices are identified by their `device_id` (which is unique within the scope of
a given user). By default, the `/login` and `/register` endpoints will
auto-generate a `device_id` and return it in the response; a client is also
free to generate its own `device_id` or, as above, reuse a device, in which
case the client should pass the `device_id` in the request body.
The lifetime of devices and `access_token`s are closely related. In the simple
case where a new device is created each time you log in, there is a one-to-one
mapping between a `device_id` and an `access_token`. If a client reuses a
`device_id` when logging in, there will be several `access_token`s associated
with a given `device_id` - but still, we would expect only one of these to be
active at once (though we do not currently enforce that in Synapse).
### Keys used in End-to-End encryption
There are a number of keys involved in encrypted communication: a summary of
them follows.
### Ed25519 fingerprint key pair
Ed25519 is a public-key cryptographic system for signing messages. In Matrix,
each device has an Ed25519 key pair which serves to identify that device. The
private part of the key pair should never leave the device, but the public part
is published to the Matrix network.
### Curve25519 identity key pair
Curve25519 is a public-key cryptographic system which can be used to establish a
shared secret. In Matrix, each device has a long-lived Curve25519 identity key
which is used to establish Olm sessions with that device. Again, the private
key should never leave the device, but the public part is signed with the
Ed25519 fingerprint key and published to the network.
Theoretically we should rotate the Curve25519 identity key from time to time,
but we haven't implemented this yet.
### Curve25519 one-time keys
As well as the identity key, each device creates a number of Curve25519 key
pairs which are also used to establish Olm sessions, but can only be used once.
Once again, the private part remains on the device.
At startup, Alice creates a number of one-time key pairs, and publishes them to
her homeserver. If Bob wants to establish an Olm session with Alice, he needs
to claim one of Alice's one-time keys, and creates a new one of his own. Those
two keys, along with Alice's and Bob's identity keys, are used in establishing
an Olm session between Alice and Bob.
### Megolm encryption keys
The Megolm key is used to encrypt group messages (in fact it is used to derive
an AES-256 key, and an HMAC-SHA-256 key). It is initialised with random data.
Each time a message is sent, a hash calculation is done on the Megolm key to
derive the key for the next message. It is therefore possible to share the
current state of the Megolm key with a user, allowing them to decrypt future
messages but not past messages.
### Ed25519 Megolm signing key pair
When a sender creates a Megolm session, he also creates another Ed25519 signing
key pair. This is used to sign messages sent via that Megolm session, to
authenticate the sender. Once again, the private part of the key remains on the
device. The public part is shared with other devices in the room alongside the
encryption key.
### Creating and registering device keys
This process only happens once, when a device first starts.
It must create the Ed25519 fingerprint key pair and the Curve25519 identity key
pair. This is done by calling `olm_create_account` in libolm. The
(base64-encoded) keys are retrieved by calling `olm_account_identity_keys`. The
account should be stored for future use.
It should then publish these keys to the homeserver, which is done by using the
`device_keys` property of the
[/keys/upload](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload)
endpoint.
In order to sign the `device_keys` payload as described in [Signing JSON
](https://matrix.org/docs/spec/appendices.html#signing-json), clients should
call `olm_account_sign`.
### Creating and registering one-time keys
The client should keep track of how many one-time keys the homeserver has stored
for it, and, if necessary, generate and upload some more.
This can be achieved by inspecting the `device_one_time_keys_count` property of
a `/sync/` response.
The maximum number of active keys supported by libolm is returned by
`olm_account_max_number_of_one_time_keys`. The client should try to maintain
about half this number on the homeserver.
To generate new one-time keys:
- Call `olm_account_generate_one_time_keys` to generate new keys.
- Call `olm_account_one_time_keys` to retrieve the unpublished keys. This
returns a JSON-formatted object with the single property `curve25519`, which
is itself an object mapping key id to base64-encoded Curve25519 key. For
example:
```json
{
"curve25519": {
"AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
"AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
}
}
```
- Each key should be signed in the same way as the previous identity keys
payload, and uploaded using the `one_time_keys` property of the
[/keys/upload](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload) endpoint.
- Call `olm_account_mark_keys_as_published` to tell the olm library not to
return the same keys from a future call to `olm_account_one_time_keys`.
### Configuring a room to use encryption
To enable encryption in a room, a client should send a state event of type
`m.room.encryption`, and content `{ "algorithm": "m.megolm.v1.aes-sha2" }`.
### Handling an `m.room.encryption` state event
When a client receives an `m.room.encryption` event as above, it should set a
flag to indicate that messages sent in the room should be encrypted.
This flag should **not** be cleared if a later `m.room.encryption` event changes
the configuration. This is to avoid a situation where a MITM can simply ask
participants to disable encryption. In short: once encryption is enabled in a
room, it can never be disabled.
The event should contain an `algorithm` property which defines which encryption
algorithm should be used for encryption. Currently only `m.megolm.v1-aes-sha2`
is permitted here.
The event may also include other settings for how messages sent in the room
should be encrypted (for example, `rotation_period_ms` to define how often the
session should be replaced). See the spec for more details.
### Handling an `m.room.encrypted` event
Encrypted events have a type of `m.room.encrypted`. They have a content property
`algorithm` which gives the encryption algorithm in use, as well as other
properties specific to the algorithm[^1].
The encrypted payload is a JSON object with the properties `type`(giving the
decrypted event type), and `content` (giving the decrypted content). Depending
on the algorithm in use, the payload may contain additional keys.
There are currently two defined algorithms:
### `m.olm.v1.curve25519-aes-sha2`
The spec gives [details on this algorithm
](https://matrix.org/docs/spec/client_server/r0.4.0.html#m-olm-v1-curve25519-aes-sha2)
and an [example payload
](https://matrix.org/docs/spec/client_server/r0.4.0.html#m-room-encrypted)
.
The `sender_key` property of the event content gives the Curve25519 identity key
of the sender. Clients should maintain a list of known Olm sessions for each
device they speak to; it is recommended to index them by Curve25519 identity
key.
Olm messages are encrypted separately for each recipient device. `ciphertext` is
an object mapping from the Curve25519 identity key for the recipient device.
The receiving client should, of course, look for its own identity key in this
object. (If it isn't listed, the message wasn't sent for it, and the client
can't decrypt it; it should show an error instead, or similar).
This should result in an object with the properties `type` and `body`. Messages
of type '0' are 'prekey' messages which are used to establish a new Olm session
between two devices; type '1' are normal messages which are used once a message
has been received on the session.
When a message (of either type) is received, a client should first attempt to
decrypt it with each of the known sessions for that sender. There are two steps
to this:
- If (and only if) `type==0`, the client should call
`olm_matches_inbound_session` with the session and `body`. This returns a
flag indicating whether the message was encrypted using that session.
- The client calls `olm_decrypt`, with the session, `type`, and `body`. If this
is successful, it returns the plaintext of the event.
If the client was unable to decrypt the message using any known sessions(or if
there are no known sessions yet), **and** the message had type 0,
**and** `olm_matches_inbound_session` wasn't true for any existing sessions,
then the client can try establishing a new session. This is done as follows:
- Call `olm_create_inbound_session_from` using the olm account, and the
`sender_key` and `body` of the message.
- If the session was established successfully:
- Call `olm_remove_one_time_keys` to ensure that the same one-time-key
cannot be reused.
- Call `olm_decrypt` with the new session.
- Store the session for future use.
At the end of this, the client will hopefully have successfully decrypted the
payload.
As well as the `type` and `content` properties, the plaintext payload should
contain a number of other properties. Each of these should be checked as
follows[^2].
- `sender`: The user ID of the sender. The client should check that this matches
the `sender` in the event.
- `recipient`: The user ID of the recipient. The client should check that this
matches the local user ID.
- `keys`: an object with a property `ed25519`. The client should check that the
value of this property matches the sender's fingerprint key when [marking the
event as verified](#marking-events-as-verified).
- `recipient_keys`: an object with a property `ed25519`. The client should check
that the value of this property matches its own fingerprint key.
### `m.megolm.v1.aes-sha2`
The spec gives [details on this algorithm
](https://matrix.org/docs/spec/client_server/r0.4.0.html#m-megolm-v1-aes-sha2)
and an [example payload
](https://matrix.org/docs/spec/client_server/r0.4.0.html#m-room-encrypted)
.
Encrypted events using this algorithm should have `sender_key`, `session_id` and
`ciphertext` content properties. If the `room_id`, `sender_key` and
`session_id` correspond to a known Megolm session (see below), the ciphertext
can be decrypted by passing the ciphertext into `olm_group_decrypt`.
In order to avoid replay attacks a client should remember the megolm
`message_index` returned by `olm_group_decrypt` of each event they decrypt for
each session. If the client decrypts an event with the same `message_index` as
one that it has already received using that session then it should treat the
message as invalid. However, care must be taken when an event is decrypted
multiple times that it is not flagged as a replay attack. For example, this may
happen when the client decrypts an event, the event gets purged from the
client's cache, and then the client backfills and re-decrypts the event. One
way to handle this case is to ensure that the record of `message_index`es is
appropriately purged when the client's cache of events is purged. Another way
is to remember the event's `event_id` and `origin_server_ts` along with its
`message_index`. When the client decrypts an event with a `message_index`
matching that of a previously-decrypted event, it can then compare the
`event_id` and `origin_server_ts` that it remembered for that `message_index`,
and if those fields match, then the message should be decrypted as normal.
The client should check that the sender's fingerprint key matches the
`keys.ed25519` property of the event which established the Megolm session when
[marking the event as verified](#marking-events-as-verified).
### Handling an `m.room_key` event
These events contain key data to allow decryption of other messages. They are
sent to specific devices, so they appear in the `to_device` section of the
response to `GET /_matrix/client/r0/sync`. They will also be encrypted, so will
need decrypting as above before they can be seen.(These events are generated by
other clients - see [starting a megolm session](#starting-a-megolm-session)).
The `room_id`, together with the `sender_key` of the `m.room_key` event before
it was decrypted, and the `session_id`, uniquely identify a Megolm session. If
they do not represent a known session, the client should start a new inbound
Megolm session by calling `olm_init_inbound_group_session` with the
`session_key`.
The client should remember the value of the keys property of the payload of the
encrypted `m.room_key` event and store it with the inbound session. This is
used as above when marking the event as verified.
### Downloading the device list for users in the room
Before an encrypted message can be sent, it is necessary to retrieve the list of
devices for each user in the room. This can be done proactively, or deferred
until the first message is sent. The information is also required to allow
users to verify or block devices.
The client should use the [/keys/query
](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query)
endpoint, passing the IDs of the members of the room in the `device_keys`
property of the request.
The client must first check the signatures on the `DeviceKeys` objects returned
by [/keys/query
](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query).
To do this, it should remove the `signatures` and `unsigned` properties, format
the remainder as Canonical JSON, and pass the result into `olm_ed25519_verify`,
using the Ed25519 key for the `key` parameter, and the corresponding signature
for the `signature` parameter. If the signature check fails, no further
processing should be done on the device.
The client must also check that the `user_id` and `device_id` fields in the
object match those in the top-level map[^3].
The client should check if the `user_id`/`device_id` correspond to a device it
had seen previously. If it did, the client **must** check that the Ed25519 key
hasn't changed. Again, if it has changed, no further processing should be done
on the device.
Otherwise the client stores the information about this device.
### Sending an encrypted message event
When sending a message in a room
[configured to use encryption](#configuring-a-room-to-use-encryption), a client
first checks to see if it has an active outbound Megolm session. If not, it first
[creates one as per below](#starting-a-megolm-session). If an outbound session
exists, it should check if it is time to [rotate](#rotating-megolm-sessions) it,
and create a new one if so.
The client then builds an encryption payload as follows:
```json
{
"type": "<event type>",
"content": "<event content>",
"room_id": "<id of destination room>"
}
```
and calls `olm_group_encrypt` to encrypt the payload. This is then packaged into
event content as follows:
```json
{
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "<our curve25519 device key>",
"ciphertext": "<encrypted payload>",
"session_id": "<outbound group session id>",
"device_id": "<our device ID>"
}
```
Finally, the encrypted event is sent to the room with
`PUT /_matrix/client/r0/rooms/<room_id>/send/m.room.encrypted/<txn_id>`.
### Starting a Megolm session
When a message is first sent in an encrypted room, the client should start a new
outbound Megolm session. This should **not** be done proactively, to avoid
proliferation of unnecessary Megolm sessions.
To create the session, the client should call `olm_init_outbound_group_session`,
and store the details of the outbound session for future use.
The client should then call `olm_outbound_group_session_id` to get the unique ID
of the new session, and `olm_outbound_group_session_key` to retrieve the
current ratchet key and index. It should store these details as an inbound
session, just as it would when
[receiving them via an m.room_key event](#handling-an-m-room-key-event).
The client must then share the keys for this session with each device in the
room. It must therefore [download the device list](#downloading-the-device-list-for-users-in-the-room) if it hasn't already done
so. Then it should build a unique `m.room_key` event, and send it encrypted
[using Olm](#encrypting-an-event-with-olm) to each device in the room which has
not been blocked.
Once all of the key-sharing event contents have been assembled, the events
should be sent to the corresponding devices via
`PUT /_matrix/client/r0/sendToDevice/m.room.encrypted/<txnId>`.
### Rotating Megolm sessions
Megolm sessions may not be reused indefinitely. The parameters which define how
often a session should be rotated are defined in the `m.room.encryption` state
event of a room.
Once either the message limit or time limit have been reached, the client should
start a new session before sending any more messages.
### Encrypting an event with Olm
Olm is not used for encrypting room events, as it requires a separate copy of
the ciphertext for each device, and because the receiving device can only
decrypt received messages once. However, it is used for encrypting key-sharing
events for Megolm.
When encrypting an event using Olm, the client should:
- Build an encryption payload as illustrated in the [spec
](https://matrix.org/docs/spec/client_server/r0.4.0.html#m-olm-v1-curve25519-aes-sha2).
- Check if it has an existing Olm session; if it does not, [start a new one
](#starting-a-megolm-session). If it has several (as may happen due to races
when establishing sessions), it should use the session from which it last
received a message.
[Starting an Olm session](#starting-an-olm-session)
- Encrypt the payload by calling `olm_encrypt`.
- Package the payload into an Olm `m.room.encrypted` event.
### Starting an Olm session
To start a new Olm session with another device, a client must first claim one of
the other device's one-time keys. To do this, it should initiate a request to
[/keys/claim
](https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim).
The client should check the signatures on the signed key objects in the
response. As with checking the signatures on the device keys, it should remove
the `signatures` and (if present) `unsigned` properties, format the remainder
as Canonical JSON, and pass the result into `olm_ed25519_verify`, using the
Ed25519 device key for the `key` parameter.
Provided the key object passes verification, the client should then pass the
key, along with the Curve25519 Identity key for the remote device, into
`olm_create_outbound_session`.
### Handling membership changes
The client should monitor rooms which are configured to use encryption for
membership changes.
When a member leaves a room, the client should invalidate any active outbound
Megolm session, to ensure that a new session is used next time the user sends a
message.
When a new member joins a room, the client should first [download the device
list](#downloading-the-device-list-for-users-in-the-room) for the new member,
if it doesn't already have it.
After giving the user an opportunity to [block
](https://matrix.org/docs/spec/client_server/r0.4.0.html#device-verification)
any suspicious devices, the client should share the keys for the outbound
Megolm session with all the new member's devices. This is done in the same way
as [creating a new session](#starting-a-megolm-session), except that there is
no need to start a new Megolm session: due to the design of the Megolm ratchet,
the new user will only be able to decrypt messages starting from the current
state. The recommended method is to maintain a list of members who are waiting
for the session keys, and share them when the user next sends a message.
### Handling new devices
When a user logs in on a new device, it is necessary to make sure that other
devices in any rooms with encryption enabled are aware of the new device, so
that they can share their outbound sessions with it as they would with a new
member.
The device tracking process which should be implemented is documented [in the
spec](https://matrix.org/docs/spec/client_server/r0.4.0.html#tracking-the-device-list-for-a-user).
### Blocking / Verifying devices
It should be possible for a user to mark each device belonging to another user
as 'Blocked' or 'Verified', through a process detailed [in the spec
](https://matrix.org/docs/spec/client_server/r0.4.0.html#sending-encrypted-attachments).
When a user chooses to block a device, this means that no further encrypted
messages should be shared with that device. In short, it should be excluded
when sharing room keys when starting a new Megolm session. Any active outbound
Megolm sessions whose keys have been shared with the device should also be
invalidated so that no further messages are sent over them.
### Marking events as 'verified'
Once a device has been verified, it is possible to verify that events have been
sent from a particular device. See the section on Handling an m.room.encrypted
event for notes on how to do this for each algorithm. Events sent from a
verified device can be decorated in the UI to show that they have been sent
from a verified device.
## Encrypted attachments
Homeservers must not be able to read files shared in encrypted rooms. Clients
should implement a strategy described [in the spec
](https://matrix.org/docs/spec/client_server/r0.4.0#sending-encrypted-attachments).
Currently, the files are encrypted using AES-CTR, which is not included in
libolm. Clients have to rely on a third party library.
## Key sharing
When an event cannot be decrypted due to missing keys, a client may want to
request them from other clients which may have them. Similarly, a client may
want to reply to a key request with the associated key if it can assert that
the requesting device is allowed to see the messages encrypted with this key.
Those capabilities are achieved using `m.room_key_request` and
`m.forwarded_room_key` events.
The `session_key` property of a `m.forwarded_room_key` event differs from the
one of a `m.room_key` event, as it does not include the Ed25519 signature of
the original sender. It should be obtained from
`olm_export_inbound_group_session` at the desired `message index`, and the
session can be restored with `olm_import_inbound_group_session`.
The `forwarded_room_key` property starts out empty, but each time a key is
forwarded to another device, the previous sender in the chain is added to the
end of the list. Consider the following example:
> - A -\> B : m.room\_key
> - B -\> C : m.forwarded\_room\_key
> - C -\> D : m.forwarded\_room\_key
In the message B -\> C `forwarded_room_key` is empty, but in the message C -\> D
it contains B's Curve25519 key. In order for D to believe that the session came
from A, D must trust the direct sender C and every entry in this chain.
In order to securely implement key sharing, clients must not reply to every key
request they receive. The recommended strategy is to share the keys
automatically only to **verified** devices of the **same user**. Requests
coming from unverified devices should prompt a dialog, allowing the user to
verify the device, share the keys without verifying, or not to share them
(and ignore future requests). A client should also check whether requests
coming from devices of other users are legitimate. This can be done by keeping
track of the users a session was shared with, and at which `message index`.
Key requests can be sent to all of the current user's devices, as well as the
original sender of the session, and other devices present in the room. When the
client receives the requested key, it should send a `m.room_key_request` event
to all the devices it requested the key from, setting the `action` property to
`"cancel_request"` and `request_id` to the ID of the initial request.
[^1]: Note that a redacted event will have an empty content, and hence the
content will have no `algorithm` property. Thus a client should check whether
an event is redacted before checking for the `algorithm` property.
[^2]: These tests prevent an attacker publishing someone else's curve25519 keys
as their own and subsequently claiming to have sent messages which they
didn't.
[^3]: This prevents a malicious or compromised homeserver replacing the keys for
the device with those of another.