blindly incorporate PR review - needs testing & fixing

This commit is contained in:
Matthew Hodgson 2017-12-18 01:52:46 +00:00 committed by Hubert Chathi
parent 69e51c7ba4
commit 0abb205b47
5 changed files with 98 additions and 78 deletions

View File

@ -289,9 +289,14 @@ class LimitExceededError(SynapseError):
class RoomKeysVersionError(SynapseError): class RoomKeysVersionError(SynapseError):
"""A client has tried to upload to a non-current version of the room_keys store """A client has tried to upload to a non-current version of the room_keys store
""" """
def __init__(self, code=403, msg="Wrong room_keys version", current_version=None, def __init__(self, current_version):
errcode=Codes.WRONG_ROOM_KEYS_VERSION): """
super(RoomKeysVersionError, self).__init__(code, msg, errcode) Args:
current_version (str): the current version of the store they should have used
"""
super(RoomKeysVersionError, self).__init__(
403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION
)
self.current_version = current_version self.current_version = current_version
def error_dict(self): def error_dict(self):

View File

@ -24,8 +24,21 @@ logger = logging.getLogger(__name__)
class E2eRoomKeysHandler(object): class E2eRoomKeysHandler(object):
"""
Implements an optional realtime backup mechanism for encrypted E2E megolm room keys.
This gives a way for users to store and recover their megolm keys if they lose all
their clients. It should also extend easily to future room key mechanisms.
The actual payload of the encrypted keys is completely opaque to the handler.
"""
def __init__(self, hs): def __init__(self, hs):
self.store = hs.get_datastore() self.store = hs.get_datastore()
# Used to lock whenever a client is uploading key data. This prevents collisions
# between clients trying to upload the details of a new session, given all
# clients belonging to a user will receive and try to upload a new session at
# roughly the same time. Also used to lock out uploads when the key is being
# changed.
self._upload_linearizer = Linearizer("upload_room_keys_lock") self._upload_linearizer = Linearizer("upload_room_keys_lock")
@defer.inlineCallbacks @defer.inlineCallbacks
@ -40,6 +53,8 @@ class E2eRoomKeysHandler(object):
@defer.inlineCallbacks @defer.inlineCallbacks
def delete_room_keys(self, user_id, version, room_id, session_id): def delete_room_keys(self, user_id, version, room_id, session_id):
# lock for consistency with uploading
with (yield self._upload_linearizer.queue(user_id)):
yield self.store.delete_e2e_room_keys(user_id, version, room_id, session_id) yield self.store.delete_e2e_room_keys(user_id, version, room_id, session_id)
@defer.inlineCallbacks @defer.inlineCallbacks
@ -47,8 +62,9 @@ class E2eRoomKeysHandler(object):
# TODO: Validate the JSON to make sure it has the right keys. # TODO: Validate the JSON to make sure it has the right keys.
# XXX: perhaps we should use a finer grained lock here?
with (yield self._upload_linearizer.queue(user_id)):
# Check that the version we're trying to upload is the current version # Check that the version we're trying to upload is the current version
try: try:
version_info = yield self.get_version_info(user_id, version) version_info = yield self.get_version_info(user_id, version)
except StoreError as e: except StoreError as e:
@ -60,13 +76,11 @@ class E2eRoomKeysHandler(object):
if version_info['version'] != version: if version_info['version'] != version:
raise RoomKeysVersionError(current_version=version_info.version) raise RoomKeysVersionError(current_version=version_info.version)
# XXX: perhaps we should use a finer grained lock here? # go through the room_keys.
with (yield self._upload_linearizer.queue(user_id)): # XXX: this should/could be done concurrently, given we're in a lock.
for room_id, room in room_keys['rooms'].iteritems():
# go through the room_keys for session_id, session in room['sessions'].iteritems():
for room_id in room_keys['rooms']: room_key = session[session_id]
for session_id in room_keys['rooms'][room_id]['sessions']:
room_key = room_keys['rooms'][room_id]['sessions'][session_id]
yield self._upload_room_key( yield self._upload_room_key(
user_id, version, room_id, session_id, room_key user_id, version, room_id, session_id, room_key
@ -86,10 +100,29 @@ class E2eRoomKeysHandler(object):
else: else:
raise e raise e
# check whether we merge or not. spelling it out with if/elifs rather if _should_replace_room_key(current_room_key, room_key):
# than lots of booleans for legibility. yield self.store.set_e2e_room_key(
upsert = True user_id, version, room_id, session_id, room_key
)
def _should_replace_room_key(current_room_key, room_key):
"""
Determine whether to replace the current_room_key in our backup for this
session (if any) with a new room_key that has been uploaded.
Args:
current_room_key (dict): Optional, the current room_key dict if any
room_key (dict): The new room_key dict which may or may not be fit to
replace the current_room_key
Returns:
True if current_room_key should be replaced by room_key in the backup
"""
if current_room_key: if current_room_key:
# spelt out with if/elifs rather than nested boolean expressions
# purely for legibility.
if room_key['is_verified'] and not current_room_key['is_verified']: if room_key['is_verified'] and not current_room_key['is_verified']:
pass pass
elif ( elif (
@ -97,16 +130,11 @@ class E2eRoomKeysHandler(object):
current_room_key['first_message_index'] current_room_key['first_message_index']
): ):
pass pass
elif room_key['forwarded_count'] < room_key['forwarded_count']: elif room_key['forwarded_count'] < current_room_key['forwarded_count']:
pass pass
else: else:
upsert = False return False
return True
# if so, we set the new room_key
if upsert:
yield self.store.set_e2e_room_key(
user_id, version, room_id, session_id, room_key
)
@defer.inlineCallbacks @defer.inlineCallbacks
def create_version(self, user_id, version_info): def create_version(self, user_id, version_info):

View File

@ -68,6 +68,8 @@ class RoomKeysServlet(RestServlet):
* lower forwarded_count always wins over higher forwarded_count * lower forwarded_count always wins over higher forwarded_count
We trust the clients not to lie and corrupt their own backups. We trust the clients not to lie and corrupt their own backups.
It also means that if your access_token is stolen, the attacker could
delete your backup.
POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1 POST /room_keys/keys/!abc:matrix.org/c0ff33?version=1 HTTP/1.1
Content-Type: application/json Content-Type: application/json

View File

@ -44,10 +44,7 @@ class EndToEndRoomKeyStore(SQLBaseStore):
def set_e2e_room_key(self, user_id, version, room_id, session_id, room_key): def set_e2e_room_key(self, user_id, version, room_id, session_id, room_key):
def _set_e2e_room_key_txn(txn): yield self._simple_upsert(
self._simple_upsert_txn(
txn,
table="e2e_room_keys", table="e2e_room_keys",
keyvalues={ keyvalues={
"user_id": user_id, "user_id": user_id,
@ -64,12 +61,6 @@ class EndToEndRoomKeyStore(SQLBaseStore):
lock=False, lock=False,
) )
return True
return self.runInteraction(
"set_e2e_room_key", _set_e2e_room_key_txn
)
# XXX: this isn't currently used and isn't tested anywhere # XXX: this isn't currently used and isn't tested anywhere
# it could be used in future for bulk-uploading new versions of room_keys # it could be used in future for bulk-uploading new versions of room_keys
# for a user or something though. # for a user or something though.
@ -107,7 +98,9 @@ class EndToEndRoomKeyStore(SQLBaseStore):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def get_e2e_room_keys(self, user_id, version, room_id, session_id): def get_e2e_room_keys(
self, user_id, version, room_id=room_id, session_id=session_id
):
keyvalues = { keyvalues = {
"user_id": user_id, "user_id": user_id,
@ -133,18 +126,10 @@ class EndToEndRoomKeyStore(SQLBaseStore):
desc="get_e2e_room_keys", desc="get_e2e_room_keys",
) )
# perlesque autovivification from https://stackoverflow.com/a/19829714/6764493 sessions = {}
class AutoVivification(dict):
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
sessions = AutoVivification()
for row in rows: for row in rows:
sessions['rooms'][row['room_id']]['sessions'][row['session_id']] = { room_entry = sessions['rooms'].setdefault(row['room_id'], {"sessions": {}})
room_entry['sessions'][row['session_id']] = {
"first_message_index": row["first_message_index"], "first_message_index": row["first_message_index"],
"forwarded_count": row["forwarded_count"], "forwarded_count": row["forwarded_count"],
"is_verified": row["is_verified"], "is_verified": row["is_verified"],
@ -154,7 +139,9 @@ class EndToEndRoomKeyStore(SQLBaseStore):
defer.returnValue(sessions) defer.returnValue(sessions)
@defer.inlineCallbacks @defer.inlineCallbacks
def delete_e2e_room_keys(self, user_id, version, room_id, session_id): def delete_e2e_room_keys(
self, user_id, version, room_id=room_id, session_id=session_id
):
keyvalues = { keyvalues = {
"user_id": user_id, "user_id": user_id,

View File

@ -25,16 +25,14 @@ CREATE TABLE e2e_room_keys (
session_data TEXT NOT NULL session_data TEXT NOT NULL
); );
CREATE UNIQUE INDEX e2e_room_keys_user_idx ON e2e_room_keys(user_id); CREATE UNIQUE INDEX e2e_room_keys_idx ON e2e_room_keys(user_id, room_id, session_id);
CREATE UNIQUE INDEX e2e_room_keys_room_idx ON e2e_room_keys(room_id);
CREATE UNIQUE INDEX e2e_room_keys_session_idx ON e2e_room_keys(session_id);
-- the metadata for each generation of encrypted e2e session backups -- the metadata for each generation of encrypted e2e session backups
CREATE TABLE e2e_room_key_versions ( CREATE TABLE e2e_room_keys_versions (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
version TEXT NOT NULL, version TEXT NOT NULL,
algorithm TEXT NOT NULL, algorithm TEXT NOT NULL,
auth_data TEXT NOT NULL auth_data TEXT NOT NULL
); );
CREATE UNIQUE INDEX e2e_room_key_user_idx ON e2e_room_keys(user_id); CREATE UNIQUE INDEX e2e_room_keys_versions_user_idx ON e2e_room_keys_versions(user_id);