diff --git a/matrix/api.py b/matrix/api.py index 00f1570..69b416c 100644 --- a/matrix/api.py +++ b/matrix/api.py @@ -94,6 +94,19 @@ class MatrixClient: return HttpRequest(RequestType.GET, self.host, path) + def room_encrypted_message(self, room_id, content): + # type: (str, Dict[Any, Any]) -> HttpRequest + query_parameters = {"access_token": self.access_token} + + path = ("{api}/rooms/{room}/send/m.room.encrypted/" + "{tx_id}?{query_parameters}").format( + api=MATRIX_API_PATH, + room=quote(room_id), + tx_id=quote(str(self._get_txn_id())), + query_parameters=urlencode(query_parameters)) + + return HttpRequest(RequestType.PUT, self.host, path, content) + def room_send_message(self, room_id, message_type, @@ -307,6 +320,18 @@ class MatrixClient: return HttpRequest(RequestType.POST, self.host, path, content) + def to_device(self, event_type, content): + query_parameters = {"access_token": self.access_token} + + path = ("{api}/sendToDevice/{event_type}/{tx_id}?" + "{query_parameters}").format( + api=MATRIX_API_PATH, + event_type=event_type, + tx_id=quote(str(self._get_txn_id())), + query_parameters=urlencode(query_parameters)) + + return HttpRequest(RequestType.PUT, self.host, path, content) + def mxc_to_http(self, mxc): # type: (str) -> str url = urlparse(mxc) @@ -648,3 +673,47 @@ class MatrixKeyClaimMessage(MatrixMessage): server) return self._decode(server, object_hook) + + +class MatrixToDeviceMessage(MatrixMessage): + def __init__(self, client, to_device_dict): + data = { + "content": to_device_dict, + "event_type": "m.room.encrypted" + } + + MatrixMessage.__init__(self, client.to_device, data) + + def decode_body(self, server): + object_hook = partial(MatrixEvents.MatrixToDeviceEvent.from_dict, + server) + + return self._decode(server, object_hook) + + +class MatrixEncryptedMessage(MatrixMessage): + + def __init__(self, + client, + room_id, + formatted_message, + content): + self.room_id = room_id + self.formatted_message = formatted_message + + data = { + "room_id": self.room_id, + "content": content + } + + MatrixMessage.__init__(self, client.room_encrypted_message, data) + + def decode_body(self, server): + object_hook = partial( + MatrixEvents.MatrixSendEvent.from_dict, + server, + self.room_id, + self.formatted_message, + ) + + return self._decode(server, object_hook) diff --git a/matrix/encryption.py b/matrix/encryption.py index 79ac911..39cc6c5 100644 --- a/matrix/encryption.py +++ b/matrix/encryption.py @@ -34,7 +34,7 @@ import matrix.globals try: from olm.account import Account, OlmAccountError from olm.session import (Session, InboundSession, OlmSessionError, - OlmPreKeyMessage) + OlmMessage, OlmPreKeyMessage) from olm.group_session import ( InboundGroupSession, OutboundGroupSession, @@ -313,6 +313,29 @@ class Olm(): except OlmSessionError: return None + def group_encrypt(self, room_id, plaintext_dict): + # type: (str, Dict[str, str]) -> Dict[str, str], Bool + is_new = False + plaintext_dict["room_id"] = room_id + + if not room_id in self.outbound_group_sessions: + self.create_outbound_group_session(room_id) + is_new = True + + session = self.outbound_group_sessions[room_id] + + ciphertext = session.encrypt(Olm._to_json(plaintext_dict)) + + payload_dict = { + "algorithm": "m.megolm.v1.aes-sha2", + "sender_key": self.account.identity_keys()["curve25519"], + "ciphertext": ciphertext, + "session_id": session.id, + "device_id": self.device_id + } + + return payload_dict, is_new + @encrypt_enabled def group_decrypt(self, room_id, session_id, ciphertext): if session_id not in self.inbound_group_sessions[room_id]: @@ -326,6 +349,69 @@ class Olm(): return plaintext + def share_group_session(self, room_id, own_id, users): + group_session = self.outbound_group_sessions[room_id] + + key_content = { + "algorithm": "m.megolm.v1.aes-sha2", + "room_id": room_id, + "session_id": group_session.id, + "session_key": group_session.session_key, + "chain_index": group_session.message_index + } + + payload_dict = { + "type": "m.room_key", + "content": key_content, + # TODO we don't have the user_id in the Olm class + "sender": own_id, + "sender_device": self.device_id, + "keys": { + "ed25519": self.account.identity_keys()["ed25519"] + } + } + + to_device_dict = { + "messages": {} + } + + for user in users: + + for key in self.device_keys[user]: + if key.device_id == self.device_id: + continue + + device_payload_dict = payload_dict.copy() + # TODO sort the sessions + session = self.sessions[user][key.device_id][0] + device_payload_dict["recipient"] = user + device_payload_dict["recipient_keys"] = { + "ed25519": key.keys["ed25519"] + } + + W.prnt("", pprint.pformat(device_payload_dict)) + + olm_message = session.encrypt(Olm._to_json(device_payload_dict)) + + olm_dict = { + "algorithm": "m.olm.v1.curve25519-aes-sha2", + "sender_key": self.account.identity_keys()["curve25519"], + "ciphertext": { + key.keys["curve25519"]: { + "type": (0 if isinstance(olm_message, + OlmPreKeyMessage) else 1), + "body": olm_message.ciphertext + } + } + } + + to_device_dict["messages"][user] = { + key.device_id: olm_dict + } + + return to_device_dict + # return {} + @classmethod @encrypt_enabled def from_session_dir(cls, user, device_id, session_path): @@ -474,6 +560,16 @@ class Olm(): return signature + @staticmethod + def _to_json(json_dict): + # type: (Dict[Any, Any]) -> str + return json.dumps( + json_dict, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True + ) + @encrypt_enabled def mark_keys_as_published(self): self.account.mark_keys_as_published() diff --git a/matrix/events.py b/matrix/events.py index ee624d9..36773a7 100644 --- a/matrix/events.py +++ b/matrix/events.py @@ -367,6 +367,23 @@ class MatrixKeyClaimEvent(MatrixEvent): pass +class MatrixToDeviceEvent(MatrixEvent): + + def __init__(self, server): + MatrixEvent.__init__(self, server) + + @classmethod + def from_dict(cls, server, parsed_dict): + try: + if parsed_dict == {}: + return cls(server) + + raise KeyError + except KeyError: + return MatrixErrorEvent.from_dict(server, ("Error sending to " + "device message"), + False, parsed_dict) + class MatrixBacklogEvent(MatrixEvent): def __init__(self, server, room_id, end_token, events): diff --git a/matrix/rooms.py b/matrix/rooms.py index ff7f7c0..3313f12 100644 --- a/matrix/rooms.py +++ b/matrix/rooms.py @@ -652,6 +652,11 @@ class RoomMemberJoin(RoomEvent): # calculate room display name and set it as the buffer list name room_name = room.display_name(server.user_id) + + # A user has joined an encrypted room, we need to check for new devices + if room.encrypted: + server.device_check_timestamp = None + W.buffer_set(buff, "short_name", room_name) diff --git a/matrix/server.py b/matrix/server.py index d32e155..21918e4 100644 --- a/matrix/server.py +++ b/matrix/server.py @@ -38,7 +38,9 @@ from matrix.api import ( MatrixSyncMessage, MatrixLoginMessage, MatrixKeyUploadMessage, - MatrixKeyQueryMessage + MatrixKeyQueryMessage, + MatrixToDeviceMessage, + MatrixEncryptedMessage ) from matrix.encryption import Olm, EncryptionError, encrypt_enabled @@ -483,6 +485,7 @@ class MatrixServer: if not room.encrypted: return + # TODO don't send messages unless all the devices are verified missing = self.olm.get_missing_sessions(room.users.keys()) if missing: @@ -494,7 +497,42 @@ class MatrixServer: # TODO claim keys for the missing user/device combinations return - # self.send_queue.append(message) + body = {"msgtype": "m.text", "body": formatted_data.to_plain()} + + if formatted_data.is_formatted(): + body["format"] = "org.matrix.custom.html" + body["formatted_body"] = formatted_data.to_html() + + plaintext_dict = { + "type": "m.room.message", + "content": body + } + + W.prnt("", "matrix: Encrypting message") + + payload_dict, session_is_new = self.olm.group_encrypt( + room_id, + plaintext_dict + ) + + if session_is_new: + to_device_dict = self.olm.share_group_session( + room_id, + self.user_id, + room.users.keys() + ) + message = MatrixToDeviceMessage(self.client, to_device_dict) + W.prnt("", "matrix: Megolm session missing for room.") + self.send_queue.append(message) + + message = MatrixEncryptedMessage( + self.client, + room_id, + formatted_data, + payload_dict + ) + + self.send_queue.append(message) @encrypt_enabled def upload_keys(self, device_keys=False, one_time_keys=False):