pull/3013/merge
Sorunome 1 month ago committed by GitHub
commit 86d7b1a53d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,358 @@
# MSC3013: Encrypted Push
Push notifications have the problem that they typically go through third-party gateways in order to
be delivered, e.g. FCM (Google) or APNs (Apple) and an app-specific gateway (sygnal). In order to
prevent these push gateways from being able to read any sensitive information the `event_id_only` format
was introduced, which only pushes the `event_id` and `room_id` of an event down the push. After
receiving the push message the client can hit the `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
to fetch the full event, and then create the notification based on that.
This, however, introduces the issue of having to perform an additional HTTP request to be able to
display the full event notification. On some systems (e.g. some weird vendor-specific android phones,
or while driving through ~~rural germany~~ places with patchy cellular network availability) this isn't
always possible.
This proposal adds a method to encrypt the push message in a way only the recipient client can decrypt
it, allowing the server to send the full event over push again, with this MSC and [MSC2782](https://github.com/matrix-org/matrix-doc/pull/2782).
## Proposal
A new pusher data field, `algorithm`, is introduced for pushers of the kind `http`. It is an enum,
represented as string. The currently allowed values are `m.plain` and `m.curve25519-aes-sha2`. If the
field is absent, then an algorithm of `m.plain` is assumed. The algorithms are defined as following:
### `m.plain` algorithm
The `m.plain` algorithm (the default algorithm) denotes that push is to be delivered in plaintext.
That is no change to current http pushers, thus this MSC is backwards compatible.
### `m.curve25519-aes-sha2` algorithm
The `m.curve25519-aes-sha2` algorithm indicates that the push payloads are to be sent encrypted.
For this, two new pusher data fields are added: `public_key` and `counts_only_type`. Both fields are
to be omitted from the actual push payloads being sent to the push gateways.
The field `public_key` is required. This key is an unpadded base64-encoded curve25519
public key. This new field is not to be added to the actual push payload being sent to push gateways.
The field `counts_only_type` is an optional enum which denotes how push frames should handle counts-only
push payloads, without any events attached to them (e.g. if the user reads a notification and thus the
unread count decreases by one). The possible values are:
- `none` (default): The unread status is never included in the plaintext payload
- `boolean`: If the push frame only affects the counts a boolean (`is_counts_only`) denoting
this is included in the plaintext payload.
- `full`: If the push frame only affects the counts the full `counts` dict is included in the plaintext
payload.
As such, setting such a pusher could look as following (assuming
[MSC2782](https://github.com/matrix-org/matrix-doc/pull/2782) has been merged):
```
POST /_matrix/client/r0/pushers/set HTTP/1.1
Content-Type: application/json
{
"lang": "en",
"kind": "http",
"app_display_name": "Mat Rix",
"device_display_name": "iPhone 9",
"profile_tag": "xxyyzz",
"app_id": "com.example.app.ios",
"pushkey": "APA91bHPRgkF3JUikC4ENAHEeMrd41Zxv3hVZjC9KtT8OvPVGJ-hQMRKRrZuJAEcl7B338qju59zJMjw2DELjzEvxwYv7hH5Ynpc1ODQ0aT4U4OFEeco8ohsN5PjL1iC2dNtk2BAokeMCg2ZXKqpc8FXKmhX94kIxQ",
"data": {
"algorithm": "m.curve25519-aes-sha2",
"url": "https://push-gateway.location.here/_matrix/push/v1/notify",
"format": "full_event",
"public_key": "GkZgmbbxnYZfFtywxF4K7NUPqA50Kb7TEsyHeVWyHBI",
"counts_only_type": "full"
},
"append": false
}
```
Now, when the homeserver pushes out the message, it is to perform the `notification` dict as with the
http pusher, and then encrypt all of its contents, apart from the `devices` key, using the following
algorithm:
1. Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key and the public key
specified when setting the pusher to generate a shared secret. The public half of the ephemeral key,
encoded using unpadded base64, becomes the `ephemeral` property of the new payload.
2. Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash, with
a salt of 32 bytes of 0, and with the empty string as the info. The first 32 bytes are used as the
AES key, the next 32 bytes are used as the MAC key, and the last 16 bytes are used as the AES
initialization vector.
3. Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding. This encrypted
data, encoded using unpadded base64, becomes the `ciphertext` property of the new payload.
4. Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key
generated above. The first 8 bytes of the resulting MAC are base64-encoded, and become the `mac`
property of the new payload.
This is the same algorithm used currently in the unstable spec for megolm backup, as such it is
comptible with libolms PkEncryption / PkDecryption methods.
Next, the `unread` or `is_counts_only` keys are optionally populated according to how the `counts_only_type`
pusher data is set.
### Example event notification:
Suppose a normal http pusher would push out the following content:
```json
{
"notification": {
"event_id": "$3957tyerfgewrf384",
"room_id": "!slw48wfj34rtnrf:example.com",
"type": "m.room.message",
"sender": "@exampleuser:matrix.org",
"sender_display_name": "Major Tom",
"room_name": "Mission Control",
"room_alias": "#exampleroom:matrix.org",
"prio": "high",
"content": {
"msgtype": "m.text",
"body": "I'm floating in a most peculiar way."
},
"counts": {
"unread": 2,
"missed_calls": 1
},
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
The following object would have to be json-encoded to encrypt:
```json
{
"event_id": "$3957tyerfgewrf384",
"room_id": "!slw48wfj34rtnrf:example.com",
"type": "m.room.message",
"sender": "@exampleuser:matrix.org",
"sender_display_name": "Major Tom",
"room_name": "Mission Control",
"room_alias": "#exampleroom:matrix.org",
"prio": "high",
"content": {
"msgtype": "m.text",
"body": "I'm floating in a most peculiar way."
},
"counts": {
"unread": 2,
"missed_calls": 1
}
}
```
Resulting in the following final message being pushed out to the push gateway:
```json
{
"notification": {
"ephemeral": "base64_of_ephemeral_public_key",
"ciphertext": "base64_of_ciphertext",
"mac": "base64_of_mac",
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {
"algorithm": "m.curve25519-aes-sha2"
},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
### Example counts only:
Suppose a normal http pusher would push out the following content:
```json
{
"notification": {
"prio": "high",
"counts": {
"unread": 2,
"missed_calls": 1
},
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
The following object would have to be json-encoded to encrypt:
```json
{
"prio": "high",
"counts": {
"unread": 2,
"missed_calls": 1
}
}
```
If `counts_only_type` is `none` the final message is:
```json
{
"notification": {
"ephemeral": "base64_of_ephemeral_public_key",
"ciphertext": "base64_of_ciphertext",
"mac": "base64_of_mac",
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {
"algorithm": "m.curve25519-aes-sha2"
},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
If `counts_only_type` is `boolean` the final message is:
```json
{
"notification": {
"ephemeral": "base64_of_ephemeral_public_key",
"ciphertext": "base64_of_ciphertext",
"mac": "base64_of_mac",
"is_counts_only": true,
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {
"algorithm": "m.curve25519-aes-sha2"
},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
If `counts_only_type` is `full` the final message is:
```json
{
"notification": {
"ephemeral": "base64_of_ephemeral_public_key",
"ciphertext": "base64_of_ciphertext",
"mac": "base64_of_mac",
"counts": {
"unread": 2,
"missed_calls": 1
},
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data": {
"algorithm": "m.curve25519-aes-sha2"
},
"tweaks": {
"sound": "bing"
}
}
]
}
}
```
## Potential issues
It is currently implied that a homeserver could push the same notification out to multiple devices
at once, by populating the `devices` array with more than one element. Due to the nature of cryptography,
this won't be possible anymore. However, homeserver implementations such as synapse
[1](https://github.com/matrix-org/synapse/blob/d6fb96e056f79de220d8d59429d89a61498e9af3/synapse/push/httppusher.py#L331-L338)
[2](https://github.com/matrix-org/synapse/blob/d6fb96e056f79de220d8d59429d89a61498e9af3/synapse/push/httppusher.py#L357-L365)
[3](https://github.com/matrix-org/synapse/blob/d6fb96e056f79de220d8d59429d89a61498e9af3/synapse/push/httppusher.py#L419-L426)
even hard-code that `devices` array to only contain a single entry, making it unlikely for this flexibility
having been used in the wild.
If the gateway does additional processing, like marking call attempts differently, the relevant data
musn't be encrypted. That means that clients which rely on that can't use this kind of pusher.
### iOS
Sadly iOS is pretty limiting concerning push notifications. While modifying the content of a push frame,
and thus e2ee push notifications, is possible on iOS, these modified push frames *have* to result in
a user-visible notification banner. This limitation becomes a problem when only the `counts` dictionary
is updated, so e.g. when a message is being read. For this, the `counts_only_type` setting has been
to this proposal, so that the push gateway can do additional logic, based on if the notification frame
is such a silent update.
If the `counts_only_type` is set to `boolean` then the push gateway could send the encrypted push payload
as an APNS background message to the device. This isn't that reliable, sadly, but might be good enough
for an app.
If the `counts_only_type` is set to `full` then the push gateway could send a badge-update notification
to APNS directly. While this works for sure, that would also mean that the APNS servers get to read
a real-time update of the exact unread count for each user.
## Alternatives
Instead of this MSC, the push gateway for a specific app could encrypt the contents so that FCM/APNs
are still unable to see them. As the push gateway is set by app, that would mean that another server
than your homeserver gets full event contents for your push notifications. So this is also questionable
from a privacy point of view.
## Security considerations
In a first draft symmetric encryption was used. However, using asymmetric encryption seems like the
proper way to go here, as, in the case of the server being compromised, there is no need to re-negotiate
a new key to encrypt the push message.
## Unstable prefix
The unstable prefix for the algorithms are `com.famedly`, meaning the unstable prefixes for the new
algorithms introduced are `com.famedly.plain` and `com.famedly.curve25519-aes-sha2`.
Additionally, the feature is to be advertised as unstable feature in the `GET /_matrix/client/versions`
response, with the key `com.famedly.msc3013` set to `true`. So, the response could look then as
following:
```json
{
"versions": ["r0.6.0"],
"unstable_features": {
"com.famedly.msc3013": true
}
}
```
Loading…
Cancel
Save