Add support for enabling encryption by default
This commit is contained in:
@@ -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]),
|
||||
),
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user