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,
|
ReceiptEventContent, PresenceEvent, PresenceState, TypingEvent,
|
||||||
MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent,
|
MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent,
|
||||||
RoomAvatarStateEventContent, RoomTopicStateEventContent,
|
RoomAvatarStateEventContent, RoomTopicStateEventContent,
|
||||||
MemberStateEventContent, EncryptedEvent)
|
MemberStateEventContent, EncryptedEvent, TextMessageEventContent,
|
||||||
|
MessageType)
|
||||||
from mautrix.errors import MatrixError
|
from mautrix.errors import MatrixError
|
||||||
|
|
||||||
from . import user as u, portal as po, puppet as pu, commands as com
|
from . import user as u, portal as po, puppet as pu, commands as com
|
||||||
@@ -38,7 +39,7 @@ except ImportError:
|
|||||||
EVENT_TIME = None
|
EVENT_TIME = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .e2ee import EncryptionManager
|
from mautrix.bridge.e2ee import EncryptionManager
|
||||||
except ImportError:
|
except ImportError:
|
||||||
EncryptionManager = None
|
EncryptionManager = None
|
||||||
|
|
||||||
@@ -57,10 +58,15 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
command_processor=com.CommandProcessor(context))
|
command_processor=com.CommandProcessor(context))
|
||||||
self.e2ee = None
|
self.e2ee = None
|
||||||
if self.config["bridge.encryption.allow"]:
|
if self.config["bridge.encryption.allow"]:
|
||||||
if EncryptionManager:
|
if not EncryptionManager:
|
||||||
self.e2ee = EncryptionManager(context)
|
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:
|
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.bot = context.bot
|
||||||
self.previously_typing = {}
|
self.previously_typing = {}
|
||||||
|
|
||||||
@@ -121,14 +127,50 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
except MatrixError:
|
except MatrixError:
|
||||||
pass
|
pass
|
||||||
portal.mxid = room_id
|
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()
|
portal.save()
|
||||||
inviter.register_portal(portal)
|
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:
|
else:
|
||||||
await intent.join_room(room_id)
|
await intent.join_room(room_id)
|
||||||
await intent.send_notice(room_id, "This puppet will remain inactive until a "
|
await intent.send_notice(room_id, "This puppet will remain inactive until a "
|
||||||
"Telegram chat is created for this room.")
|
"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:
|
async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None:
|
||||||
try:
|
try:
|
||||||
is_management = len(await self.az.intent.get_room_members(room_id)) == 2
|
is_management = len(await self.az.intent.get_room_members(room_id)) == 2
|
||||||
@@ -173,7 +215,7 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
"messages for unauthenticated users.")
|
"messages for unauthenticated users.")
|
||||||
return
|
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:
|
if await user.is_logged_in() or portal.has_bot:
|
||||||
await portal.join_matrix(user, event_id)
|
await portal.join_matrix(user, event_id)
|
||||||
|
|
||||||
|
|||||||
@@ -308,6 +308,17 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
||||||
"content": 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"]:
|
if config["appservice.community_id"]:
|
||||||
initial_state.append({
|
initial_state.append({
|
||||||
"type": "m.room.related_groups",
|
"type": "m.room.related_groups",
|
||||||
@@ -325,6 +336,13 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
if not room_id:
|
if not room_id:
|
||||||
raise Exception(f"Failed to create room")
|
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.mxid = RoomID(room_id)
|
||||||
self.by_mxid[self.mxid] = self
|
self.by_mxid[self.mxid] = self
|
||||||
self.save()
|
self.save()
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17
|
|||||||
python-magic>=0.4,<0.5
|
python-magic>=0.4,<0.5
|
||||||
commonmark>=0.8,<0.10
|
commonmark>=0.8,<0.10
|
||||||
aiohttp>=3,<4
|
aiohttp>=3,<4
|
||||||
mautrix==0.5.0.beta5
|
mautrix==0.5.0.beta6
|
||||||
telethon>=1.10,<1.12
|
telethon>=1.10,<1.12
|
||||||
telethon-session-sqlalchemy>=0.2.14,<0.3
|
telethon-session-sqlalchemy>=0.2.14,<0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user