Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 03c1103

Browse files
authoredOct 15, 2018
Merge pull request #4019 from matrix-org/dbkr/e2e_backups
E2E backups
2 parents f9ce1b4 + a45f2c3 commit 03c1103

File tree

10 files changed

+1442
-0
lines changed

10 files changed

+1442
-0
lines changed
 

‎changelog.d/4019.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for end-to-end key backup (MSC1687)

‎synapse/api/errors.py

+15
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Codes(object):
5959
RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
6060
UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
6161
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
62+
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
6263

6364

6465
class CodeMessageException(RuntimeError):
@@ -312,6 +313,20 @@ def error_dict(self):
312313
)
313314

314315

316+
class RoomKeysVersionError(SynapseError):
317+
"""A client has tried to upload to a non-current version of the room_keys store
318+
"""
319+
def __init__(self, current_version):
320+
"""
321+
Args:
322+
current_version (str): the current version of the store they should have used
323+
"""
324+
super(RoomKeysVersionError, self).__init__(
325+
403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION
326+
)
327+
self.current_version = current_version
328+
329+
315330
class IncompatibleRoomVersionError(SynapseError):
316331
"""A server is trying to join a room whose version it does not support."""
317332

‎synapse/handlers/e2e_room_keys.py

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017, 2018 New Vector Ltd
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import logging
17+
18+
from six import iteritems
19+
20+
from twisted.internet import defer
21+
22+
from synapse.api.errors import RoomKeysVersionError, StoreError, SynapseError
23+
from synapse.util.async_helpers import Linearizer
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class E2eRoomKeysHandler(object):
29+
"""
30+
Implements an optional realtime backup mechanism for encrypted E2E megolm room keys.
31+
This gives a way for users to store and recover their megolm keys if they lose all
32+
their clients. It should also extend easily to future room key mechanisms.
33+
The actual payload of the encrypted keys is completely opaque to the handler.
34+
"""
35+
36+
def __init__(self, hs):
37+
self.store = hs.get_datastore()
38+
39+
# Used to lock whenever a client is uploading key data. This prevents collisions
40+
# between clients trying to upload the details of a new session, given all
41+
# clients belonging to a user will receive and try to upload a new session at
42+
# roughly the same time. Also used to lock out uploads when the key is being
43+
# changed.
44+
self._upload_linearizer = Linearizer("upload_room_keys_lock")
45+
46+
@defer.inlineCallbacks
47+
def get_room_keys(self, user_id, version, room_id=None, session_id=None):
48+
"""Bulk get the E2E room keys for a given backup, optionally filtered to a given
49+
room, or a given session.
50+
See EndToEndRoomKeyStore.get_e2e_room_keys for full details.
51+
52+
Args:
53+
user_id(str): the user whose keys we're getting
54+
version(str): the version ID of the backup we're getting keys from
55+
room_id(string): room ID to get keys for, for None to get keys for all rooms
56+
session_id(string): session ID to get keys for, for None to get keys for all
57+
sessions
58+
Returns:
59+
A deferred list of dicts giving the session_data and message metadata for
60+
these room keys.
61+
"""
62+
63+
# we deliberately take the lock to get keys so that changing the version
64+
# works atomically
65+
with (yield self._upload_linearizer.queue(user_id)):
66+
results = yield self.store.get_e2e_room_keys(
67+
user_id, version, room_id, session_id
68+
)
69+
70+
if results['rooms'] == {}:
71+
raise SynapseError(404, "No room_keys found")
72+
73+
defer.returnValue(results)
74+
75+
@defer.inlineCallbacks
76+
def delete_room_keys(self, user_id, version, room_id=None, session_id=None):
77+
"""Bulk delete the E2E room keys for a given backup, optionally filtered to a given
78+
room or a given session.
79+
See EndToEndRoomKeyStore.delete_e2e_room_keys for full details.
80+
81+
Args:
82+
user_id(str): the user whose backup we're deleting
83+
version(str): the version ID of the backup we're deleting
84+
room_id(string): room ID to delete keys for, for None to delete keys for all
85+
rooms
86+
session_id(string): session ID to delete keys for, for None to delete keys
87+
for all sessions
88+
Returns:
89+
A deferred of the deletion transaction
90+
"""
91+
92+
# lock for consistency with uploading
93+
with (yield self._upload_linearizer.queue(user_id)):
94+
yield self.store.delete_e2e_room_keys(user_id, version, room_id, session_id)
95+
96+
@defer.inlineCallbacks
97+
def upload_room_keys(self, user_id, version, room_keys):
98+
"""Bulk upload a list of room keys into a given backup version, asserting
99+
that the given version is the current backup version. room_keys are merged
100+
into the current backup as described in RoomKeysServlet.on_PUT().
101+
102+
Args:
103+
user_id(str): the user whose backup we're setting
104+
version(str): the version ID of the backup we're updating
105+
room_keys(dict): a nested dict describing the room_keys we're setting:
106+
107+
{
108+
"rooms": {
109+
"!abc:matrix.org": {
110+
"sessions": {
111+
"c0ff33": {
112+
"first_message_index": 1,
113+
"forwarded_count": 1,
114+
"is_verified": false,
115+
"session_data": "SSBBTSBBIEZJU0gK"
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
Raises:
123+
SynapseError: with code 404 if there are no versions defined
124+
RoomKeysVersionError: if the uploaded version is not the current version
125+
"""
126+
127+
# TODO: Validate the JSON to make sure it has the right keys.
128+
129+
# XXX: perhaps we should use a finer grained lock here?
130+
with (yield self._upload_linearizer.queue(user_id)):
131+
132+
# Check that the version we're trying to upload is the current version
133+
try:
134+
version_info = yield self.store.get_e2e_room_keys_version_info(user_id)
135+
except StoreError as e:
136+
if e.code == 404:
137+
raise SynapseError(404, "Version '%s' not found" % (version,))
138+
else:
139+
raise
140+
141+
if version_info['version'] != version:
142+
# Check that the version we're trying to upload actually exists
143+
try:
144+
version_info = yield self.store.get_e2e_room_keys_version_info(
145+
user_id, version,
146+
)
147+
# if we get this far, the version must exist
148+
raise RoomKeysVersionError(current_version=version_info['version'])
149+
except StoreError as e:
150+
if e.code == 404:
151+
raise SynapseError(404, "Version '%s' not found" % (version,))
152+
else:
153+
raise
154+
155+
# go through the room_keys.
156+
# XXX: this should/could be done concurrently, given we're in a lock.
157+
for room_id, room in iteritems(room_keys['rooms']):
158+
for session_id, session in iteritems(room['sessions']):
159+
yield self._upload_room_key(
160+
user_id, version, room_id, session_id, session
161+
)
162+
163+
@defer.inlineCallbacks
164+
def _upload_room_key(self, user_id, version, room_id, session_id, room_key):
165+
"""Upload a given room_key for a given room and session into a given
166+
version of the backup. Merges the key with any which might already exist.
167+
168+
Args:
169+
user_id(str): the user whose backup we're setting
170+
version(str): the version ID of the backup we're updating
171+
room_id(str): the ID of the room whose keys we're setting
172+
session_id(str): the session whose room_key we're setting
173+
room_key(dict): the room_key being set
174+
"""
175+
176+
# get the room_key for this particular row
177+
current_room_key = None
178+
try:
179+
current_room_key = yield self.store.get_e2e_room_key(
180+
user_id, version, room_id, session_id
181+
)
182+
except StoreError as e:
183+
if e.code == 404:
184+
pass
185+
else:
186+
raise
187+
188+
if self._should_replace_room_key(current_room_key, room_key):
189+
yield self.store.set_e2e_room_key(
190+
user_id, version, room_id, session_id, room_key
191+
)
192+
193+
@staticmethod
194+
def _should_replace_room_key(current_room_key, room_key):
195+
"""
196+
Determine whether to replace a given current_room_key (if any)
197+
with a newly uploaded room_key backup
198+
199+
Args:
200+
current_room_key (dict): Optional, the current room_key dict if any
201+
room_key (dict): The new room_key dict which may or may not be fit to
202+
replace the current_room_key
203+
204+
Returns:
205+
True if current_room_key should be replaced by room_key in the backup
206+
"""
207+
208+
if current_room_key:
209+
# spelt out with if/elifs rather than nested boolean expressions
210+
# purely for legibility.
211+
212+
if room_key['is_verified'] and not current_room_key['is_verified']:
213+
return True
214+
elif (
215+
room_key['first_message_index'] <
216+
current_room_key['first_message_index']
217+
):
218+
return True
219+
elif room_key['forwarded_count'] < current_room_key['forwarded_count']:
220+
return True
221+
else:
222+
return False
223+
return True
224+
225+
@defer.inlineCallbacks
226+
def create_version(self, user_id, version_info):
227+
"""Create a new backup version. This automatically becomes the new
228+
backup version for the user's keys; previous backups will no longer be
229+
writeable to.
230+
231+
Args:
232+
user_id(str): the user whose backup version we're creating
233+
version_info(dict): metadata about the new version being created
234+
235+
{
236+
"algorithm": "m.megolm_backup.v1",
237+
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
238+
}
239+
240+
Returns:
241+
A deferred of a string that gives the new version number.
242+
"""
243+
244+
# TODO: Validate the JSON to make sure it has the right keys.
245+
246+
# lock everyone out until we've switched version
247+
with (yield self._upload_linearizer.queue(user_id)):
248+
new_version = yield self.store.create_e2e_room_keys_version(
249+
user_id, version_info
250+
)
251+
defer.returnValue(new_version)
252+
253+
@defer.inlineCallbacks
254+
def get_version_info(self, user_id, version=None):
255+
"""Get the info about a given version of the user's backup
256+
257+
Args:
258+
user_id(str): the user whose current backup version we're querying
259+
version(str): Optional; if None gives the most recent version
260+
otherwise a historical one.
261+
Raises:
262+
StoreError: code 404 if the requested backup version doesn't exist
263+
Returns:
264+
A deferred of a info dict that gives the info about the new version.
265+
266+
{
267+
"version": "1234",
268+
"algorithm": "m.megolm_backup.v1",
269+
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
270+
}
271+
"""
272+
273+
with (yield self._upload_linearizer.queue(user_id)):
274+
res = yield self.store.get_e2e_room_keys_version_info(user_id, version)
275+
defer.returnValue(res)
276+
277+
@defer.inlineCallbacks
278+
def delete_version(self, user_id, version=None):
279+
"""Deletes a given version of the user's e2e_room_keys backup
280+
281+
Args:
282+
user_id(str): the user whose current backup version we're deleting
283+
version(str): the version id of the backup being deleted
284+
Raises:
285+
StoreError: code 404 if this backup version doesn't exist
286+
"""
287+
288+
with (yield self._upload_linearizer.queue(user_id)):
289+
yield self.store.delete_e2e_room_keys_version(user_id, version)

‎synapse/rest/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
receipts,
4747
register,
4848
report_event,
49+
room_keys,
4950
sendtodevice,
5051
sync,
5152
tags,
@@ -102,6 +103,7 @@ def register_servlets(client_resource, hs):
102103
auth.register_servlets(hs, client_resource)
103104
receipts.register_servlets(hs, client_resource)
104105
read_marker.register_servlets(hs, client_resource)
106+
room_keys.register_servlets(hs, client_resource)
105107
keys.register_servlets(hs, client_resource)
106108
tokenrefresh.register_servlets(hs, client_resource)
107109
tags.register_servlets(hs, client_resource)
+372
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017, 2018 New Vector Ltd
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import logging
17+
18+
from twisted.internet import defer
19+
20+
from synapse.api.errors import Codes, SynapseError
21+
from synapse.http.servlet import (
22+
RestServlet,
23+
parse_json_object_from_request,
24+
parse_string,
25+
)
26+
27+
from ._base import client_v2_patterns
28+
29+
logger = logging.getLogger(__name__)
30+
31+
32+
class RoomKeysServlet(RestServlet):
33+
PATTERNS = client_v2_patterns(
34+
"/room_keys/keys(/(?P<room_id>[^/]+))?(/(?P<session_id>[^/]+))?$"
35+
)
36+
37+
def __init__(self, hs):
38+
"""
39+
Args:
40+
hs (synapse.server.HomeServer): server
41+
"""
42+
super(RoomKeysServlet, self).__init__()
43+
self.auth = hs.get_auth()
44+
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
45+
46+
@defer.inlineCallbacks
47+
def on_PUT(self, request, room_id, session_id):
48+
"""
49+
Uploads one or more encrypted E2E room keys for backup purposes.
50+
room_id: the ID of the room the keys are for (optional)
51+
session_id: the ID for the E2E room keys for the room (optional)
52+
version: the version of the user's backup which this data is for.
53+
the version must already have been created via the /room_keys/version API.
54+
55+
Each session has:
56+
* first_message_index: a numeric index indicating the oldest message
57+
encrypted by this session.
58+
* forwarded_count: how many times the uploading client claims this key
59+
has been shared (forwarded)
60+
* is_verified: whether the client that uploaded the keys claims they
61+
were sent by a device which they've verified
62+
* session_data: base64-encrypted data describing the session.
63+
64+
Returns 200 OK on success with body {}
65+
Returns 403 Forbidden if the version in question is not the most recently
66+
created version (i.e. if this is an old client trying to write to a stale backup)
67+
Returns 404 Not Found if the version in question doesn't exist
68+
69+
The API is designed to be otherwise agnostic to the room_key encryption
70+
algorithm being used. Sessions are merged with existing ones in the
71+
backup using the heuristics:
72+
* is_verified sessions always win over unverified sessions
73+
* older first_message_index always win over newer sessions
74+
* lower forwarded_count always wins over higher forwarded_count
75+
76+
We trust the clients not to lie and corrupt their own backups.
77+
It also means that if your access_token is stolen, the attacker could
78+
delete your backup.
79+
80+
POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1
81+
Content-Type: application/json
82+
83+
{
84+
"first_message_index": 1,
85+
"forwarded_count": 1,
86+
"is_verified": false,
87+
"session_data": "SSBBTSBBIEZJU0gK"
88+
}
89+
90+
Or...
91+
92+
POST /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1
93+
Content-Type: application/json
94+
95+
{
96+
"sessions": {
97+
"c0ff33": {
98+
"first_message_index": 1,
99+
"forwarded_count": 1,
100+
"is_verified": false,
101+
"session_data": "SSBBTSBBIEZJU0gK"
102+
}
103+
}
104+
}
105+
106+
Or...
107+
108+
POST /room_keys/keys?version=1 HTTP/1.1
109+
Content-Type: application/json
110+
111+
{
112+
"rooms": {
113+
"!abc:matrix.org": {
114+
"sessions": {
115+
"c0ff33": {
116+
"first_message_index": 1,
117+
"forwarded_count": 1,
118+
"is_verified": false,
119+
"session_data": "SSBBTSBBIEZJU0gK"
120+
}
121+
}
122+
}
123+
}
124+
}
125+
"""
126+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
127+
user_id = requester.user.to_string()
128+
body = parse_json_object_from_request(request)
129+
version = parse_string(request, "version")
130+
131+
if session_id:
132+
body = {
133+
"sessions": {
134+
session_id: body
135+
}
136+
}
137+
138+
if room_id:
139+
body = {
140+
"rooms": {
141+
room_id: body
142+
}
143+
}
144+
145+
yield self.e2e_room_keys_handler.upload_room_keys(
146+
user_id, version, body
147+
)
148+
defer.returnValue((200, {}))
149+
150+
@defer.inlineCallbacks
151+
def on_GET(self, request, room_id, session_id):
152+
"""
153+
Retrieves one or more encrypted E2E room keys for backup purposes.
154+
Symmetric with the PUT version of the API.
155+
156+
room_id: the ID of the room to retrieve the keys for (optional)
157+
session_id: the ID for the E2E room keys to retrieve the keys for (optional)
158+
version: the version of the user's backup which this data is for.
159+
the version must already have been created via the /change_secret API.
160+
161+
Returns as follows:
162+
163+
GET /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1
164+
{
165+
"first_message_index": 1,
166+
"forwarded_count": 1,
167+
"is_verified": false,
168+
"session_data": "SSBBTSBBIEZJU0gK"
169+
}
170+
171+
Or...
172+
173+
GET /room_keys/keys/!abc:matrix.org?version=1 HTTP/1.1
174+
{
175+
"sessions": {
176+
"c0ff33": {
177+
"first_message_index": 1,
178+
"forwarded_count": 1,
179+
"is_verified": false,
180+
"session_data": "SSBBTSBBIEZJU0gK"
181+
}
182+
}
183+
}
184+
185+
Or...
186+
187+
GET /room_keys/keys?version=1 HTTP/1.1
188+
{
189+
"rooms": {
190+
"!abc:matrix.org": {
191+
"sessions": {
192+
"c0ff33": {
193+
"first_message_index": 1,
194+
"forwarded_count": 1,
195+
"is_verified": false,
196+
"session_data": "SSBBTSBBIEZJU0gK"
197+
}
198+
}
199+
}
200+
}
201+
}
202+
"""
203+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
204+
user_id = requester.user.to_string()
205+
version = parse_string(request, "version")
206+
207+
room_keys = yield self.e2e_room_keys_handler.get_room_keys(
208+
user_id, version, room_id, session_id
209+
)
210+
211+
if session_id:
212+
room_keys = room_keys['rooms'][room_id]['sessions'][session_id]
213+
elif room_id:
214+
room_keys = room_keys['rooms'][room_id]
215+
216+
defer.returnValue((200, room_keys))
217+
218+
@defer.inlineCallbacks
219+
def on_DELETE(self, request, room_id, session_id):
220+
"""
221+
Deletes one or more encrypted E2E room keys for a user for backup purposes.
222+
223+
DELETE /room_keys/keys/!abc:matrix.org/c0ff33?version=1
224+
HTTP/1.1 200 OK
225+
{}
226+
227+
room_id: the ID of the room whose keys to delete (optional)
228+
session_id: the ID for the E2E session to delete (optional)
229+
version: the version of the user's backup which this data is for.
230+
the version must already have been created via the /change_secret API.
231+
"""
232+
233+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
234+
user_id = requester.user.to_string()
235+
version = parse_string(request, "version")
236+
237+
yield self.e2e_room_keys_handler.delete_room_keys(
238+
user_id, version, room_id, session_id
239+
)
240+
defer.returnValue((200, {}))
241+
242+
243+
class RoomKeysNewVersionServlet(RestServlet):
244+
PATTERNS = client_v2_patterns(
245+
"/room_keys/version$"
246+
)
247+
248+
def __init__(self, hs):
249+
"""
250+
Args:
251+
hs (synapse.server.HomeServer): server
252+
"""
253+
super(RoomKeysNewVersionServlet, self).__init__()
254+
self.auth = hs.get_auth()
255+
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
256+
257+
@defer.inlineCallbacks
258+
def on_POST(self, request):
259+
"""
260+
Create a new backup version for this user's room_keys with the given
261+
info. The version is allocated by the server and returned to the user
262+
in the response. This API is intended to be used whenever the user
263+
changes the encryption key for their backups, ensuring that backups
264+
encrypted with different keys don't collide.
265+
266+
It takes out an exclusive lock on this user's room_key backups, to ensure
267+
clients only upload to the current backup.
268+
269+
The algorithm passed in the version info is a reverse-DNS namespaced
270+
identifier to describe the format of the encrypted backupped keys.
271+
272+
The auth_data is { user_id: "user_id", nonce: <random string> }
273+
encrypted using the algorithm and current encryption key described above.
274+
275+
POST /room_keys/version
276+
Content-Type: application/json
277+
{
278+
"algorithm": "m.megolm_backup.v1",
279+
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
280+
}
281+
282+
HTTP/1.1 200 OK
283+
Content-Type: application/json
284+
{
285+
"version": 12345
286+
}
287+
"""
288+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
289+
user_id = requester.user.to_string()
290+
info = parse_json_object_from_request(request)
291+
292+
new_version = yield self.e2e_room_keys_handler.create_version(
293+
user_id, info
294+
)
295+
defer.returnValue((200, {"version": new_version}))
296+
297+
# we deliberately don't have a PUT /version, as these things really should
298+
# be immutable to avoid people footgunning
299+
300+
301+
class RoomKeysVersionServlet(RestServlet):
302+
PATTERNS = client_v2_patterns(
303+
"/room_keys/version(/(?P<version>[^/]+))?$"
304+
)
305+
306+
def __init__(self, hs):
307+
"""
308+
Args:
309+
hs (synapse.server.HomeServer): server
310+
"""
311+
super(RoomKeysVersionServlet, self).__init__()
312+
self.auth = hs.get_auth()
313+
self.e2e_room_keys_handler = hs.get_e2e_room_keys_handler()
314+
315+
@defer.inlineCallbacks
316+
def on_GET(self, request, version):
317+
"""
318+
Retrieve the version information about a given version of the user's
319+
room_keys backup. If the version part is missing, returns info about the
320+
most current backup version (if any)
321+
322+
It takes out an exclusive lock on this user's room_key backups, to ensure
323+
clients only upload to the current backup.
324+
325+
Returns 404 if the given version does not exist.
326+
327+
GET /room_keys/version/12345 HTTP/1.1
328+
{
329+
"version": "12345",
330+
"algorithm": "m.megolm_backup.v1",
331+
"auth_data": "dGhpcyBzaG91bGQgYWN0dWFsbHkgYmUgZW5jcnlwdGVkIGpzb24K"
332+
}
333+
"""
334+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
335+
user_id = requester.user.to_string()
336+
337+
try:
338+
info = yield self.e2e_room_keys_handler.get_version_info(
339+
user_id, version
340+
)
341+
except SynapseError as e:
342+
if e.code == 404:
343+
raise SynapseError(404, "No backup found", Codes.NOT_FOUND)
344+
defer.returnValue((200, info))
345+
346+
@defer.inlineCallbacks
347+
def on_DELETE(self, request, version):
348+
"""
349+
Delete the information about a given version of the user's
350+
room_keys backup. If the version part is missing, deletes the most
351+
current backup version (if any). Doesn't delete the actual room data.
352+
353+
DELETE /room_keys/version/12345 HTTP/1.1
354+
HTTP/1.1 200 OK
355+
{}
356+
"""
357+
if version is None:
358+
raise SynapseError(400, "No version specified to delete", Codes.NOT_FOUND)
359+
360+
requester = yield self.auth.get_user_by_req(request, allow_guest=False)
361+
user_id = requester.user.to_string()
362+
363+
yield self.e2e_room_keys_handler.delete_version(
364+
user_id, version
365+
)
366+
defer.returnValue((200, {}))
367+
368+
369+
def register_servlets(hs, http_server):
370+
RoomKeysServlet(hs).register(http_server)
371+
RoomKeysVersionServlet(hs).register(http_server)
372+
RoomKeysNewVersionServlet(hs).register(http_server)

‎synapse/server.py

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from synapse.handlers.device import DeviceHandler
5252
from synapse.handlers.devicemessage import DeviceMessageHandler
5353
from synapse.handlers.e2e_keys import E2eKeysHandler
54+
from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler
5455
from synapse.handlers.events import EventHandler, EventStreamHandler
5556
from synapse.handlers.groups_local import GroupsLocalHandler
5657
from synapse.handlers.initial_sync import InitialSyncHandler
@@ -130,6 +131,7 @@ def build_DEPENDENCY(self)
130131
'auth_handler',
131132
'device_handler',
132133
'e2e_keys_handler',
134+
'e2e_room_keys_handler',
133135
'event_handler',
134136
'event_stream_handler',
135137
'initial_sync_handler',
@@ -299,6 +301,9 @@ def build_device_message_handler(self):
299301
def build_e2e_keys_handler(self):
300302
return E2eKeysHandler(self)
301303

304+
def build_e2e_room_keys_handler(self):
305+
return E2eRoomKeysHandler(self)
306+
302307
def build_application_service_api(self):
303308
return ApplicationServiceApi(self)
304309

‎synapse/storage/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .client_ips import ClientIpStore
3131
from .deviceinbox import DeviceInboxStore
3232
from .directory import DirectoryStore
33+
from .e2e_room_keys import EndToEndRoomKeyStore
3334
from .end_to_end_keys import EndToEndKeyStore
3435
from .engines import PostgresEngine
3536
from .event_federation import EventFederationStore
@@ -77,6 +78,7 @@ class DataStore(RoomMemberStore, RoomStore,
7778
ApplicationServiceTransactionStore,
7879
ReceiptsStore,
7980
EndToEndKeyStore,
81+
EndToEndRoomKeyStore,
8082
SearchStore,
8183
TagsStore,
8284
AccountDataStore,

‎synapse/storage/e2e_room_keys.py

+320
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 New Vector Ltd
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import json
17+
18+
from twisted.internet import defer
19+
20+
from synapse.api.errors import StoreError
21+
22+
from ._base import SQLBaseStore
23+
24+
25+
class EndToEndRoomKeyStore(SQLBaseStore):
26+
27+
@defer.inlineCallbacks
28+
def get_e2e_room_key(self, user_id, version, room_id, session_id):
29+
"""Get the encrypted E2E room key for a given session from a given
30+
backup version of room_keys. We only store the 'best' room key for a given
31+
session at a given time, as determined by the handler.
32+
33+
Args:
34+
user_id(str): the user whose backup we're querying
35+
version(str): the version ID of the backup for the set of keys we're querying
36+
room_id(str): the ID of the room whose keys we're querying.
37+
This is a bit redundant as it's implied by the session_id, but
38+
we include for consistency with the rest of the API.
39+
session_id(str): the session whose room_key we're querying.
40+
41+
Returns:
42+
A deferred dict giving the session_data and message metadata for
43+
this room key.
44+
"""
45+
46+
row = yield self._simple_select_one(
47+
table="e2e_room_keys",
48+
keyvalues={
49+
"user_id": user_id,
50+
"version": version,
51+
"room_id": room_id,
52+
"session_id": session_id,
53+
},
54+
retcols=(
55+
"first_message_index",
56+
"forwarded_count",
57+
"is_verified",
58+
"session_data",
59+
),
60+
desc="get_e2e_room_key",
61+
)
62+
63+
row["session_data"] = json.loads(row["session_data"])
64+
65+
defer.returnValue(row)
66+
67+
@defer.inlineCallbacks
68+
def set_e2e_room_key(self, user_id, version, room_id, session_id, room_key):
69+
"""Replaces or inserts the encrypted E2E room key for a given session in
70+
a given backup
71+
72+
Args:
73+
user_id(str): the user whose backup we're setting
74+
version(str): the version ID of the backup we're updating
75+
room_id(str): the ID of the room whose keys we're setting
76+
session_id(str): the session whose room_key we're setting
77+
room_key(dict): the room_key being set
78+
Raises:
79+
StoreError
80+
"""
81+
82+
yield self._simple_upsert(
83+
table="e2e_room_keys",
84+
keyvalues={
85+
"user_id": user_id,
86+
"room_id": room_id,
87+
"session_id": session_id,
88+
},
89+
values={
90+
"version": version,
91+
"first_message_index": room_key['first_message_index'],
92+
"forwarded_count": room_key['forwarded_count'],
93+
"is_verified": room_key['is_verified'],
94+
"session_data": json.dumps(room_key['session_data']),
95+
},
96+
lock=False,
97+
)
98+
99+
@defer.inlineCallbacks
100+
def get_e2e_room_keys(
101+
self, user_id, version, room_id=None, session_id=None
102+
):
103+
"""Bulk get the E2E room keys for a given backup, optionally filtered to a given
104+
room, or a given session.
105+
106+
Args:
107+
user_id(str): the user whose backup we're querying
108+
version(str): the version ID of the backup for the set of keys we're querying
109+
room_id(str): Optional. the ID of the room whose keys we're querying, if any.
110+
If not specified, we return the keys for all the rooms in the backup.
111+
session_id(str): Optional. the session whose room_key we're querying, if any.
112+
If specified, we also require the room_id to be specified.
113+
If not specified, we return all the keys in this version of
114+
the backup (or for the specified room)
115+
116+
Returns:
117+
A deferred list of dicts giving the session_data and message metadata for
118+
these room keys.
119+
"""
120+
121+
keyvalues = {
122+
"user_id": user_id,
123+
"version": version,
124+
}
125+
if room_id:
126+
keyvalues['room_id'] = room_id
127+
if session_id:
128+
keyvalues['session_id'] = session_id
129+
130+
rows = yield self._simple_select_list(
131+
table="e2e_room_keys",
132+
keyvalues=keyvalues,
133+
retcols=(
134+
"user_id",
135+
"room_id",
136+
"session_id",
137+
"first_message_index",
138+
"forwarded_count",
139+
"is_verified",
140+
"session_data",
141+
),
142+
desc="get_e2e_room_keys",
143+
)
144+
145+
sessions = {'rooms': {}}
146+
for row in rows:
147+
room_entry = sessions['rooms'].setdefault(row['room_id'], {"sessions": {}})
148+
room_entry['sessions'][row['session_id']] = {
149+
"first_message_index": row["first_message_index"],
150+
"forwarded_count": row["forwarded_count"],
151+
"is_verified": row["is_verified"],
152+
"session_data": json.loads(row["session_data"]),
153+
}
154+
155+
defer.returnValue(sessions)
156+
157+
@defer.inlineCallbacks
158+
def delete_e2e_room_keys(
159+
self, user_id, version, room_id=None, session_id=None
160+
):
161+
"""Bulk delete the E2E room keys for a given backup, optionally filtered to a given
162+
room or a given session.
163+
164+
Args:
165+
user_id(str): the user whose backup we're deleting from
166+
version(str): the version ID of the backup for the set of keys we're deleting
167+
room_id(str): Optional. the ID of the room whose keys we're deleting, if any.
168+
If not specified, we delete the keys for all the rooms in the backup.
169+
session_id(str): Optional. the session whose room_key we're querying, if any.
170+
If specified, we also require the room_id to be specified.
171+
If not specified, we delete all the keys in this version of
172+
the backup (or for the specified room)
173+
174+
Returns:
175+
A deferred of the deletion transaction
176+
"""
177+
178+
keyvalues = {
179+
"user_id": user_id,
180+
"version": version,
181+
}
182+
if room_id:
183+
keyvalues['room_id'] = room_id
184+
if session_id:
185+
keyvalues['session_id'] = session_id
186+
187+
yield self._simple_delete(
188+
table="e2e_room_keys",
189+
keyvalues=keyvalues,
190+
desc="delete_e2e_room_keys",
191+
)
192+
193+
@staticmethod
194+
def _get_current_version(txn, user_id):
195+
txn.execute(
196+
"SELECT MAX(version) FROM e2e_room_keys_versions "
197+
"WHERE user_id=? AND deleted=0",
198+
(user_id,)
199+
)
200+
row = txn.fetchone()
201+
if not row:
202+
raise StoreError(404, 'No current backup version')
203+
return row[0]
204+
205+
def get_e2e_room_keys_version_info(self, user_id, version=None):
206+
"""Get info metadata about a version of our room_keys backup.
207+
208+
Args:
209+
user_id(str): the user whose backup we're querying
210+
version(str): Optional. the version ID of the backup we're querying about
211+
If missing, we return the information about the current version.
212+
Raises:
213+
StoreError: with code 404 if there are no e2e_room_keys_versions present
214+
Returns:
215+
A deferred dict giving the info metadata for this backup version
216+
"""
217+
218+
def _get_e2e_room_keys_version_info_txn(txn):
219+
if version is None:
220+
this_version = self._get_current_version(txn, user_id)
221+
else:
222+
this_version = version
223+
224+
result = self._simple_select_one_txn(
225+
txn,
226+
table="e2e_room_keys_versions",
227+
keyvalues={
228+
"user_id": user_id,
229+
"version": this_version,
230+
"deleted": 0,
231+
},
232+
retcols=(
233+
"version",
234+
"algorithm",
235+
"auth_data",
236+
),
237+
)
238+
result["auth_data"] = json.loads(result["auth_data"])
239+
return result
240+
241+
return self.runInteraction(
242+
"get_e2e_room_keys_version_info",
243+
_get_e2e_room_keys_version_info_txn
244+
)
245+
246+
def create_e2e_room_keys_version(self, user_id, info):
247+
"""Atomically creates a new version of this user's e2e_room_keys store
248+
with the given version info.
249+
250+
Args:
251+
user_id(str): the user whose backup we're creating a version
252+
info(dict): the info about the backup version to be created
253+
254+
Returns:
255+
A deferred string for the newly created version ID
256+
"""
257+
258+
def _create_e2e_room_keys_version_txn(txn):
259+
txn.execute(
260+
"SELECT MAX(version) FROM e2e_room_keys_versions WHERE user_id=?",
261+
(user_id,)
262+
)
263+
current_version = txn.fetchone()[0]
264+
if current_version is None:
265+
current_version = '0'
266+
267+
new_version = str(int(current_version) + 1)
268+
269+
self._simple_insert_txn(
270+
txn,
271+
table="e2e_room_keys_versions",
272+
values={
273+
"user_id": user_id,
274+
"version": new_version,
275+
"algorithm": info["algorithm"],
276+
"auth_data": json.dumps(info["auth_data"]),
277+
},
278+
)
279+
280+
return new_version
281+
282+
return self.runInteraction(
283+
"create_e2e_room_keys_version_txn", _create_e2e_room_keys_version_txn
284+
)
285+
286+
def delete_e2e_room_keys_version(self, user_id, version=None):
287+
"""Delete a given backup version of the user's room keys.
288+
Doesn't delete their actual key data.
289+
290+
Args:
291+
user_id(str): the user whose backup version we're deleting
292+
version(str): Optional. the version ID of the backup version we're deleting
293+
If missing, we delete the current backup version info.
294+
Raises:
295+
StoreError: with code 404 if there are no e2e_room_keys_versions present,
296+
or if the version requested doesn't exist.
297+
"""
298+
299+
def _delete_e2e_room_keys_version_txn(txn):
300+
if version is None:
301+
this_version = self._get_current_version(txn, user_id)
302+
else:
303+
this_version = version
304+
305+
return self._simple_update_one_txn(
306+
txn,
307+
table="e2e_room_keys_versions",
308+
keyvalues={
309+
"user_id": user_id,
310+
"version": this_version,
311+
},
312+
updatevalues={
313+
"deleted": 1,
314+
}
315+
)
316+
317+
return self.runInteraction(
318+
"delete_e2e_room_keys_version",
319+
_delete_e2e_room_keys_version_txn
320+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Copyright 2017 New Vector Ltd
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- users' optionally backed up encrypted e2e sessions
17+
CREATE TABLE e2e_room_keys (
18+
user_id TEXT NOT NULL,
19+
room_id TEXT NOT NULL,
20+
session_id TEXT NOT NULL,
21+
version TEXT NOT NULL,
22+
first_message_index INT,
23+
forwarded_count INT,
24+
is_verified BOOLEAN,
25+
session_data TEXT NOT NULL
26+
);
27+
28+
CREATE UNIQUE INDEX e2e_room_keys_idx ON e2e_room_keys(user_id, room_id, session_id);
29+
30+
-- the metadata for each generation of encrypted e2e session backups
31+
CREATE TABLE e2e_room_keys_versions (
32+
user_id TEXT NOT NULL,
33+
version TEXT NOT NULL,
34+
algorithm TEXT NOT NULL,
35+
auth_data TEXT NOT NULL,
36+
deleted SMALLINT DEFAULT 0 NOT NULL
37+
);
38+
39+
CREATE UNIQUE INDEX e2e_room_keys_versions_idx ON e2e_room_keys_versions(user_id, version);

‎tests/handlers/test_e2e_room_keys.py

+397
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 OpenMarket Ltd
3+
# Copyright 2017 New Vector Ltd
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import copy
18+
19+
import mock
20+
21+
from twisted.internet import defer
22+
23+
import synapse.api.errors
24+
import synapse.handlers.e2e_room_keys
25+
import synapse.storage
26+
from synapse.api import errors
27+
28+
from tests import unittest, utils
29+
30+
# sample room_key data for use in the tests
31+
room_keys = {
32+
"rooms": {
33+
"!abc:matrix.org": {
34+
"sessions": {
35+
"c0ff33": {
36+
"first_message_index": 1,
37+
"forwarded_count": 1,
38+
"is_verified": False,
39+
"session_data": "SSBBTSBBIEZJU0gK"
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
47+
class E2eRoomKeysHandlerTestCase(unittest.TestCase):
48+
def __init__(self, *args, **kwargs):
49+
super(E2eRoomKeysHandlerTestCase, self).__init__(*args, **kwargs)
50+
self.hs = None # type: synapse.server.HomeServer
51+
self.handler = None # type: synapse.handlers.e2e_keys.E2eRoomKeysHandler
52+
53+
@defer.inlineCallbacks
54+
def setUp(self):
55+
self.hs = yield utils.setup_test_homeserver(
56+
self.addCleanup,
57+
handlers=None,
58+
replication_layer=mock.Mock(),
59+
)
60+
self.handler = synapse.handlers.e2e_room_keys.E2eRoomKeysHandler(self.hs)
61+
self.local_user = "@boris:" + self.hs.hostname
62+
63+
@defer.inlineCallbacks
64+
def test_get_missing_current_version_info(self):
65+
"""Check that we get a 404 if we ask for info about the current version
66+
if there is no version.
67+
"""
68+
res = None
69+
try:
70+
yield self.handler.get_version_info(self.local_user)
71+
except errors.SynapseError as e:
72+
res = e.code
73+
self.assertEqual(res, 404)
74+
75+
@defer.inlineCallbacks
76+
def test_get_missing_version_info(self):
77+
"""Check that we get a 404 if we ask for info about a specific version
78+
if it doesn't exist.
79+
"""
80+
res = None
81+
try:
82+
yield self.handler.get_version_info(self.local_user, "bogus_version")
83+
except errors.SynapseError as e:
84+
res = e.code
85+
self.assertEqual(res, 404)
86+
87+
@defer.inlineCallbacks
88+
def test_create_version(self):
89+
"""Check that we can create and then retrieve versions.
90+
"""
91+
res = yield self.handler.create_version(self.local_user, {
92+
"algorithm": "m.megolm_backup.v1",
93+
"auth_data": "first_version_auth_data",
94+
})
95+
self.assertEqual(res, "1")
96+
97+
# check we can retrieve it as the current version
98+
res = yield self.handler.get_version_info(self.local_user)
99+
self.assertDictEqual(res, {
100+
"version": "1",
101+
"algorithm": "m.megolm_backup.v1",
102+
"auth_data": "first_version_auth_data",
103+
})
104+
105+
# check we can retrieve it as a specific version
106+
res = yield self.handler.get_version_info(self.local_user, "1")
107+
self.assertDictEqual(res, {
108+
"version": "1",
109+
"algorithm": "m.megolm_backup.v1",
110+
"auth_data": "first_version_auth_data",
111+
})
112+
113+
# upload a new one...
114+
res = yield self.handler.create_version(self.local_user, {
115+
"algorithm": "m.megolm_backup.v1",
116+
"auth_data": "second_version_auth_data",
117+
})
118+
self.assertEqual(res, "2")
119+
120+
# check we can retrieve it as the current version
121+
res = yield self.handler.get_version_info(self.local_user)
122+
self.assertDictEqual(res, {
123+
"version": "2",
124+
"algorithm": "m.megolm_backup.v1",
125+
"auth_data": "second_version_auth_data",
126+
})
127+
128+
@defer.inlineCallbacks
129+
def test_delete_missing_version(self):
130+
"""Check that we get a 404 on deleting nonexistent versions
131+
"""
132+
res = None
133+
try:
134+
yield self.handler.delete_version(self.local_user, "1")
135+
except errors.SynapseError as e:
136+
res = e.code
137+
self.assertEqual(res, 404)
138+
139+
@defer.inlineCallbacks
140+
def test_delete_missing_current_version(self):
141+
"""Check that we get a 404 on deleting nonexistent current version
142+
"""
143+
res = None
144+
try:
145+
yield self.handler.delete_version(self.local_user)
146+
except errors.SynapseError as e:
147+
res = e.code
148+
self.assertEqual(res, 404)
149+
150+
@defer.inlineCallbacks
151+
def test_delete_version(self):
152+
"""Check that we can create and then delete versions.
153+
"""
154+
res = yield self.handler.create_version(self.local_user, {
155+
"algorithm": "m.megolm_backup.v1",
156+
"auth_data": "first_version_auth_data",
157+
})
158+
self.assertEqual(res, "1")
159+
160+
# check we can delete it
161+
yield self.handler.delete_version(self.local_user, "1")
162+
163+
# check that it's gone
164+
res = None
165+
try:
166+
yield self.handler.get_version_info(self.local_user, "1")
167+
except errors.SynapseError as e:
168+
res = e.code
169+
self.assertEqual(res, 404)
170+
171+
@defer.inlineCallbacks
172+
def test_get_missing_room_keys(self):
173+
"""Check that we get a 404 on querying missing room_keys
174+
"""
175+
res = None
176+
try:
177+
yield self.handler.get_room_keys(self.local_user, "bogus_version")
178+
except errors.SynapseError as e:
179+
res = e.code
180+
self.assertEqual(res, 404)
181+
182+
# check we also get a 404 even if the version is valid
183+
version = yield self.handler.create_version(self.local_user, {
184+
"algorithm": "m.megolm_backup.v1",
185+
"auth_data": "first_version_auth_data",
186+
})
187+
self.assertEqual(version, "1")
188+
189+
res = None
190+
try:
191+
yield self.handler.get_room_keys(self.local_user, version)
192+
except errors.SynapseError as e:
193+
res = e.code
194+
self.assertEqual(res, 404)
195+
196+
# TODO: test the locking semantics when uploading room_keys,
197+
# although this is probably best done in sytest
198+
199+
@defer.inlineCallbacks
200+
def test_upload_room_keys_no_versions(self):
201+
"""Check that we get a 404 on uploading keys when no versions are defined
202+
"""
203+
res = None
204+
try:
205+
yield self.handler.upload_room_keys(self.local_user, "no_version", room_keys)
206+
except errors.SynapseError as e:
207+
res = e.code
208+
self.assertEqual(res, 404)
209+
210+
@defer.inlineCallbacks
211+
def test_upload_room_keys_bogus_version(self):
212+
"""Check that we get a 404 on uploading keys when an nonexistent version
213+
is specified
214+
"""
215+
version = yield self.handler.create_version(self.local_user, {
216+
"algorithm": "m.megolm_backup.v1",
217+
"auth_data": "first_version_auth_data",
218+
})
219+
self.assertEqual(version, "1")
220+
221+
res = None
222+
try:
223+
yield self.handler.upload_room_keys(
224+
self.local_user, "bogus_version", room_keys
225+
)
226+
except errors.SynapseError as e:
227+
res = e.code
228+
self.assertEqual(res, 404)
229+
230+
@defer.inlineCallbacks
231+
def test_upload_room_keys_wrong_version(self):
232+
"""Check that we get a 403 on uploading keys for an old version
233+
"""
234+
version = yield self.handler.create_version(self.local_user, {
235+
"algorithm": "m.megolm_backup.v1",
236+
"auth_data": "first_version_auth_data",
237+
})
238+
self.assertEqual(version, "1")
239+
240+
version = yield self.handler.create_version(self.local_user, {
241+
"algorithm": "m.megolm_backup.v1",
242+
"auth_data": "second_version_auth_data",
243+
})
244+
self.assertEqual(version, "2")
245+
246+
res = None
247+
try:
248+
yield self.handler.upload_room_keys(self.local_user, "1", room_keys)
249+
except errors.SynapseError as e:
250+
res = e.code
251+
self.assertEqual(res, 403)
252+
253+
@defer.inlineCallbacks
254+
def test_upload_room_keys_insert(self):
255+
"""Check that we can insert and retrieve keys for a session
256+
"""
257+
version = yield self.handler.create_version(self.local_user, {
258+
"algorithm": "m.megolm_backup.v1",
259+
"auth_data": "first_version_auth_data",
260+
})
261+
self.assertEqual(version, "1")
262+
263+
yield self.handler.upload_room_keys(self.local_user, version, room_keys)
264+
265+
res = yield self.handler.get_room_keys(self.local_user, version)
266+
self.assertDictEqual(res, room_keys)
267+
268+
# check getting room_keys for a given room
269+
res = yield self.handler.get_room_keys(
270+
self.local_user,
271+
version,
272+
room_id="!abc:matrix.org"
273+
)
274+
self.assertDictEqual(res, room_keys)
275+
276+
# check getting room_keys for a given session_id
277+
res = yield self.handler.get_room_keys(
278+
self.local_user,
279+
version,
280+
room_id="!abc:matrix.org",
281+
session_id="c0ff33",
282+
)
283+
self.assertDictEqual(res, room_keys)
284+
285+
@defer.inlineCallbacks
286+
def test_upload_room_keys_merge(self):
287+
"""Check that we can upload a new room_key for an existing session and
288+
have it correctly merged"""
289+
version = yield self.handler.create_version(self.local_user, {
290+
"algorithm": "m.megolm_backup.v1",
291+
"auth_data": "first_version_auth_data",
292+
})
293+
self.assertEqual(version, "1")
294+
295+
yield self.handler.upload_room_keys(self.local_user, version, room_keys)
296+
297+
new_room_keys = copy.deepcopy(room_keys)
298+
new_room_key = new_room_keys['rooms']['!abc:matrix.org']['sessions']['c0ff33']
299+
300+
# test that increasing the message_index doesn't replace the existing session
301+
new_room_key['first_message_index'] = 2
302+
new_room_key['session_data'] = 'new'
303+
yield self.handler.upload_room_keys(self.local_user, version, new_room_keys)
304+
305+
res = yield self.handler.get_room_keys(self.local_user, version)
306+
self.assertEqual(
307+
res['rooms']['!abc:matrix.org']['sessions']['c0ff33']['session_data'],
308+
"SSBBTSBBIEZJU0gK"
309+
)
310+
311+
# test that marking the session as verified however /does/ replace it
312+
new_room_key['is_verified'] = True
313+
yield self.handler.upload_room_keys(self.local_user, version, new_room_keys)
314+
315+
res = yield self.handler.get_room_keys(self.local_user, version)
316+
self.assertEqual(
317+
res['rooms']['!abc:matrix.org']['sessions']['c0ff33']['session_data'],
318+
"new"
319+
)
320+
321+
# test that a session with a higher forwarded_count doesn't replace one
322+
# with a lower forwarding count
323+
new_room_key['forwarded_count'] = 2
324+
new_room_key['session_data'] = 'other'
325+
yield self.handler.upload_room_keys(self.local_user, version, new_room_keys)
326+
327+
res = yield self.handler.get_room_keys(self.local_user, version)
328+
self.assertEqual(
329+
res['rooms']['!abc:matrix.org']['sessions']['c0ff33']['session_data'],
330+
"new"
331+
)
332+
333+
# TODO: check edge cases as well as the common variations here
334+
335+
@defer.inlineCallbacks
336+
def test_delete_room_keys(self):
337+
"""Check that we can insert and delete keys for a session
338+
"""
339+
version = yield self.handler.create_version(self.local_user, {
340+
"algorithm": "m.megolm_backup.v1",
341+
"auth_data": "first_version_auth_data",
342+
})
343+
self.assertEqual(version, "1")
344+
345+
# check for bulk-delete
346+
yield self.handler.upload_room_keys(self.local_user, version, room_keys)
347+
yield self.handler.delete_room_keys(self.local_user, version)
348+
res = None
349+
try:
350+
yield self.handler.get_room_keys(
351+
self.local_user,
352+
version,
353+
room_id="!abc:matrix.org",
354+
session_id="c0ff33",
355+
)
356+
except errors.SynapseError as e:
357+
res = e.code
358+
self.assertEqual(res, 404)
359+
360+
# check for bulk-delete per room
361+
yield self.handler.upload_room_keys(self.local_user, version, room_keys)
362+
yield self.handler.delete_room_keys(
363+
self.local_user,
364+
version,
365+
room_id="!abc:matrix.org",
366+
)
367+
res = None
368+
try:
369+
yield self.handler.get_room_keys(
370+
self.local_user,
371+
version,
372+
room_id="!abc:matrix.org",
373+
session_id="c0ff33",
374+
)
375+
except errors.SynapseError as e:
376+
res = e.code
377+
self.assertEqual(res, 404)
378+
379+
# check for bulk-delete per session
380+
yield self.handler.upload_room_keys(self.local_user, version, room_keys)
381+
yield self.handler.delete_room_keys(
382+
self.local_user,
383+
version,
384+
room_id="!abc:matrix.org",
385+
session_id="c0ff33",
386+
)
387+
res = None
388+
try:
389+
yield self.handler.get_room_keys(
390+
self.local_user,
391+
version,
392+
room_id="!abc:matrix.org",
393+
session_id="c0ff33",
394+
)
395+
except errors.SynapseError as e:
396+
res = e.code
397+
self.assertEqual(res, 404)

0 commit comments

Comments
 (0)
This repository has been archived.