Add support for enabling encryption by default

This commit is contained in:
Tulir Asokan
2020-03-29 01:37:00 +02:00
parent 260c1612a6
commit 56d21bdf59
4 changed files with 68 additions and 120 deletions
-112
View File
@@ -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 <https://www.gnu.org/licenses/>.
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]),
),
)
+49 -7
View File
@@ -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)
+18
View File
@@ -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()
+1 -1
View File
@@ -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