diff --git a/mautrix_telegram/e2ee.py b/mautrix_telegram/e2ee.py deleted file mode 100644 index cb9d6895..00000000 --- a/mautrix_telegram/e2ee.py +++ /dev/null @@ -1,112 +0,0 @@ -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2019 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -from typing import Tuple, Union -import logging -import asyncio -import hashlib -import hmac - -from nio import AsyncClient, Event as NioEvent, GroupEncryptionError, LoginError - -from mautrix.appservice import AppService -from mautrix.types import (Filter, RoomFilter, EventFilter, RoomEventFilter, StateFilter, - EventType, RoomID, Serializable, JSON, MessageEvent, Event) - -from .context import Context - - -class EncryptionManager: - loop: asyncio.AbstractEventLoop - log: logging.Logger = logging.getLogger("mau.e2ee") - client: AsyncClient - az: AppService - - login_shared_secret: bytes - - sync_task: asyncio.Task - - def __init__(self, context: 'Context') -> None: - self.loop = context.loop - self.az = context.az - self.config = context.config - lss: str = self.config["bridge.login_shared_secret"] - if not lss: - raise ValueError("login_shared_secret must be set to enable encryption") - self.login_shared_secret = lss.encode("utf-8") - self.client = AsyncClient(homeserver=self.config["homeserver.address"], - user=self.az.bot_mxid, device_id="Telegram bridge", - store_path="nio_store") - - async def encrypt(self, room_id: RoomID, event_type: EventType, - content: Union[Serializable, JSON]) -> Tuple[EventType, JSON]: - serialized = content.serialize() if isinstance(content, Serializable) else content - type_str = str(event_type) - retries = 0 - while True: - try: - type_str, encrypted = self.client.encrypt(room_id, type_str, serialized) - break - except GroupEncryptionError: - if retries > 3: - self.log.error("Got GroupEncryptionError again, giving up") - raise - retries += 1 - self.log.debug("Got GroupEncryptionError, sharing group session and trying again") - await self.client.share_group_session(room_id, ignore_unverified_devices=True) - event_type = EventType.find(type_str) - try: - encrypted["m.relates_to"] = serialized["m.relates_to"] - except KeyError: - pass - return event_type, encrypted - - def decrypt(self, event: MessageEvent) -> MessageEvent: - serialized = event.serialize() - event = self.client.decrypt_event(NioEvent.parse_encrypted_event(serialized)) - try: - event.source["content"]["m.relates_to"] = serialized["content"]["m.relates_to"] - except KeyError: - pass - return Event.deserialize(event.source) - - async def start(self) -> None: - self.log.debug("Logging in with bridge bot user") - password = hmac.new(self.login_shared_secret, self.az.bot_mxid.encode("utf-8"), - hashlib.sha512).hexdigest() - resp = await self.client.login(password, device_name="Telegram bridge") - if isinstance(resp, LoginError): - raise resp - self.sync_task = self.loop.create_task(self.client.sync_forever( - timeout=30000, sync_filter=self._filter.serialize())) - self.log.info("End-to-bridge encryption support is enabled") - - def stop(self) -> None: - self.sync_task.cancel() - - @property - def _filter(self) -> Filter: - all_events = EventType.find("*") - return Filter( - account_data=EventFilter(types=[all_events]), - presence=EventFilter(not_types=[all_events]), - room=RoomFilter( - include_leave=False, - state=StateFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), - timeline=RoomEventFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), - account_data=RoomEventFilter(not_types=[all_events]), - ephemeral=RoomEventFilter(not_types=[all_events]), - ), - ) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 3163e679..8e1b0038 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -20,7 +20,8 @@ from mautrix.types import (Event, EventType, RoomID, UserID, EventID, ReceiptEve ReceiptEventContent, PresenceEvent, PresenceState, TypingEvent, MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent, RoomAvatarStateEventContent, RoomTopicStateEventContent, - MemberStateEventContent, EncryptedEvent) + MemberStateEventContent, EncryptedEvent, TextMessageEventContent, + MessageType) from mautrix.errors import MatrixError from . import user as u, portal as po, puppet as pu, commands as com @@ -38,7 +39,7 @@ except ImportError: EVENT_TIME = None try: - from .e2ee import EncryptionManager + from mautrix.bridge.e2ee import EncryptionManager except ImportError: EncryptionManager = None @@ -57,10 +58,15 @@ class MatrixHandler(BaseMatrixHandler): command_processor=com.CommandProcessor(context)) self.e2ee = None if self.config["bridge.encryption.allow"]: - if EncryptionManager: - self.e2ee = EncryptionManager(context) + if not EncryptionManager: + self.log.error("Encryption enabled in config, but dependencies not installed.") + elif not self.config["bridge.login_shared_secret"]: + self.log.warning("Encryption enabled in config, but login_shared_secret not set.") else: - self.log.warning("Encryption enabled in config, but dependencies not installed.") + self.e2ee = EncryptionManager( + bot_mxid=self.az.bot_mxid, + login_shared_secret=self.config["bridge.login_shared_secret"], + homeserver_address=self.config["homeserver.address"], loop=context.loop) self.bot = context.bot self.previously_typing = {} @@ -121,14 +127,50 @@ class MatrixHandler(BaseMatrixHandler): except MatrixError: pass portal.mxid = room_id + e2be_ok = None + if self.config["bridge.encryption.default"] and self.e2ee: + e2be_ok = await self._enable_dm_encryption(portal) portal.save() inviter.register_portal(portal) - await intent.send_notice(room_id, "Portal to private chat created.") + if e2be_ok is True: + evt_type, content = await self.e2ee.encrypt( + room_id, EventType.ROOM_MESSAGE, + TextMessageEventContent(msgtype=MessageType.NOTICE, + body="Portal to private chat created and end-to-bridge" + " encryption enabled.")) + await intent.send_message_event(room_id, evt_type, content) + else: + message = "Portal to private chat created." + if e2be_ok is False: + message += "\n\nWarning: Failed to enable end-to-bridge encryption" + await intent.send_notice(room_id, message) else: await intent.join_room(room_id) await intent.send_notice(room_id, "This puppet will remain inactive until a " "Telegram chat is created for this room.") + async def _enable_dm_encryption(self, portal: po.Portal) -> bool: + try: + await portal.main_intent.invite_user(portal.mxid, self.az.bot_mxid) + await self.az.intent.join_room_by_id(portal.mxid) + await portal.main_intent.send_state_event(portal.mxid, EventType.ROOM_ENCRYPTION, { + "algorithm": "m.megolm.v1.aes-sha2" + }) + # TODO feed info about room to matrix-nio + except Exception: + self.log.warning(f"Failed to enable end-to-bridge encryption in {portal.mxid}", + exc_info=True) + return False + + try: + puppet = pu.Puppet.get(portal.tgid) + await portal.main_intent.set_room_name(portal.mxid, puppet.displayname) + except Exception: + self.log.warning(f"Failed to set room name for {portal.mxid}", exc_info=True) + + portal.encrypted = True + return True + async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None: try: is_management = len(await self.az.intent.get_room_members(room_id)) == 2 @@ -173,7 +215,7 @@ class MatrixHandler(BaseMatrixHandler): "messages for unauthenticated users.") return - self.log.debug(f"{user} joined {room_id}") + self.log.debug(f"{user.mxid} joined {room_id}") if await user.is_logged_in() or portal.has_bot: await portal.join_matrix(user, event_id) diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 4d4d491f..3c1a00d3 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -308,6 +308,17 @@ class PortalMetadata(BasePortal, ABC): "type": EventType.ROOM_POWER_LEVELS.serialize(), "content": power_levels.serialize(), }] + if config["bridge.encryption.default"] and self.matrix.e2ee: + self.encrypted = True + initial_state.append({ + "type": "m.room.encryption", + "content": {"algorithm": "m.megolm.v1.aes-sha2"}, + }) + if direct: + invites.append(self.az.bot_mxid) + # The bridge bot needs to join for e2ee, but that messes up the default name generation + # If/when canonical DMs happen, this might not be necessary anymore. + self.title = puppet.displayname if config["appservice.community_id"]: initial_state.append({ "type": "m.room.related_groups", @@ -325,6 +336,13 @@ class PortalMetadata(BasePortal, ABC): if not room_id: raise Exception(f"Failed to create room") + if self.encrypted and direct: + try: + await self.az.intent.join_room_by_id(room_id) + # TODO feed info about room to matrix-nio + except Exception: + self.log.warning(f"Failed to add bridge bot to new private chat portal {room_id}") + self.mxid = RoomID(room_id) self.by_mxid[self.mxid] = self self.save() diff --git a/requirements.txt b/requirements.txt index e841e270..7e3a246e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta5 +mautrix==0.5.0.beta6 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3