From 12d7c48ca7c15fd3ff61608369af1cf69e289aeb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 18 Sep 2020 20:48:29 +0300 Subject: [PATCH] Switch to appservice login as specified in matrix-org/matrix-doc#2778 --- mautrix/__init__.py | 2 +- mautrix/bridge/e2ee.py | 26 ++++++++++++++------------ mautrix/bridge/matrix.py | 24 +++++++++++++----------- mautrix/client/api/authentication.py | 6 +++--- mautrix/types/__init__.py | 2 +- mautrix/types/auth.py | 19 +++++++++++++++++-- 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/mautrix/__init__.py b/mautrix/__init__.py index 207dfe9f..baf76fbc 100644 --- a/mautrix/__init__.py +++ b/mautrix/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.7.3" +__version__ = "0.8.0+dev" __author__ = "Tulir Asokan " __all__ = ["api", "appservice", "bridge", "client", "crypto", "errors", "util", "types"] diff --git a/mautrix/bridge/e2ee.py b/mautrix/bridge/e2ee.py index 021e53a1..823851e4 100644 --- a/mautrix/bridge/e2ee.py +++ b/mautrix/bridge/e2ee.py @@ -6,12 +6,11 @@ from typing import Tuple, Union, Optional, Dict, TYPE_CHECKING import logging import asyncio -import hashlib -import hmac from mautrix.types import (Filter, RoomFilter, EventFilter, RoomEventFilter, StateFilter, EventType, RoomID, Serializable, JSON, MessageEvent, EncryptedEvent, StateEvent, - EncryptedMegolmEventContent, RequestedKeyInfo, RoomKeyWithheldCode) + EncryptedMegolmEventContent, RequestedKeyInfo, RoomKeyWithheldCode, + LoginType) from mautrix.appservice import AppService from mautrix.errors import EncryptionError from mautrix.client import Client, SyncStore @@ -19,7 +18,7 @@ DeviceIdentity, RejectKeyShare, TrustState) from mautrix.util.logging import TraceLogger -from .crypto_state_store import GetPortalFunc, PgCryptoStateStore, SQLCryptoStateStore +from .crypto_state_store import PgCryptoStateStore, SQLCryptoStateStore try: from mautrix.client.state_store.sqlalchemy import UserProfile @@ -47,23 +46,21 @@ class EncryptionManager: bridge: 'Bridge' az: AppService - login_shared_secret: bytes _id_prefix: str _id_suffix: str sync_task: asyncio.Future _share_session_events: Dict[RoomID, asyncio.Event] - def __init__(self, bridge: 'Bridge', login_shared_secret: str, homeserver_address: str, - user_id_prefix: str, user_id_suffix: str, db_url: str, - key_sharing_config: Dict[str, bool] = None) -> None: + def __init__(self, bridge: 'Bridge', homeserver_address: str, user_id_prefix: str, + user_id_suffix: str, db_url: str, key_sharing_config: Dict[str, bool] = None + ) -> None: self.loop = bridge.loop or asyncio.get_event_loop() self.bridge = bridge self.az = bridge.az self.device_name = bridge.name self._id_prefix = user_id_prefix self._id_suffix = user_id_suffix - self.login_shared_secret = login_shared_secret.encode("utf-8") self._share_session_events = {} self.key_sharing_config = key_sharing_config or {} pickle_key = "mautrix.bridge.e2ee" @@ -161,17 +158,22 @@ async def decrypt(self, evt: EncryptedEvent) -> MessageEvent: self.log.trace("Decrypted event %s: %s", evt.event_id, decrypted) return decrypted + async def check_server_support(self) -> bool: + flows = await self.client.get_login_flows() + return flows.supports_type(LoginType.APPSERVICE) + 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() if self.crypto_db: await self.crypto_db.start() await self.crypto_store.open() device_id = await self.crypto_store.get_device_id() if device_id: self.log.debug(f"Found device ID in database: {device_id}") - await self.client.login(password=password, device_name=self.device_name, + # We set the API token to the AS token here to authenticate the appservice login + # It'll get overridden after the login + self.client.api.token = self.az.as_token + await self.client.login(login_type=LoginType.APPSERVICE, device_name=self.device_name, device_id=device_id, store_access_token=True, update_hs_url=False) await self.crypto.load() if not device_id: diff --git a/mautrix/bridge/matrix.py b/mautrix/bridge/matrix.py index cbbfa8e0..dfbeb882 100644 --- a/mautrix/bridge/matrix.py +++ b/mautrix/bridge/matrix.py @@ -57,16 +57,13 @@ def __init__(self, command_processor: Optional[CommandProcessor] = None, if self.config["bridge.encryption.allow"]: 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.e2ee = EncryptionManager( - bridge=bridge, - user_id_prefix=self.user_id_prefix, user_id_suffix=self.user_id_suffix, - login_shared_secret=self.config["bridge.login_shared_secret"], - homeserver_address=self.config["homeserver.address"], - db_url=self._get_db_url(bridge.config), - key_sharing_config=self.config["bridge.encryption.key_sharing"]) + return + self.e2ee = EncryptionManager( + bridge=bridge, + user_id_prefix=self.user_id_prefix, user_id_suffix=self.user_id_suffix, + homeserver_address=self.config["homeserver.address"], + db_url=self._get_db_url(bridge.config), + key_sharing_config=self.config["bridge.encryption.key_sharing"]) @staticmethod def _get_db_url(config: 'BaseBridgeConfig') -> str: @@ -123,7 +120,12 @@ async def init_as_bot(self) -> None: async def init_encryption(self) -> None: if self.e2ee: - await self.e2ee.start() + if not await self.e2ee.check_server_support(): + self.log.error("Encryption enabled in config, but homeserver does not " + "support appservice login") + self.e2ee = None + else: + await self.e2ee.start() @staticmethod async def allow_message(user: 'BaseUser') -> bool: diff --git a/mautrix/client/api/authentication.py b/mautrix/client/api/authentication.py index 9218ddaf..4eabab7f 100644 --- a/mautrix/client/api/authentication.py +++ b/mautrix/client/api/authentication.py @@ -7,7 +7,7 @@ from mautrix.errors import MatrixResponseError from mautrix.api import Method, Path -from mautrix.types import (UserID, LoginType, UserIdentifier, LoginResponse, LoginFlow, +from mautrix.types import (UserID, LoginType, UserIdentifier, LoginResponse, LoginFlowList, MatrixUserIdentifier) from .base import BaseClientAPI @@ -24,7 +24,7 @@ class ClientAuthenticationMethods(BaseClientAPI): # region 5.5 Login # API reference: https://matrix.org/docs/spec/client_server/r0.6.1.html#login - async def get_login_flows(self) -> List[LoginFlow]: + async def get_login_flows(self) -> LoginFlowList: """ Get login flows supported by the homeserver. @@ -35,7 +35,7 @@ async def get_login_flows(self) -> List[LoginFlow]: """ resp = await self.api.request(Method.GET, Path.login) try: - return [LoginFlow.deserialize(flow) for flow in resp["flows"]] + return LoginFlowList.deserialize(resp) except KeyError: raise MatrixResponseError("`flows` not in response.") diff --git a/mautrix/types/__init__.py b/mautrix/types/__init__.py index 865ede91..d8326cdc 100644 --- a/mautrix/types/__init__.py +++ b/mautrix/types/__init__.py @@ -41,7 +41,7 @@ from .auth import (LoginType, UserIdentifierType, MatrixUserIdentifier, ThirdPartyIdentifier, PhoneIdentifier, UserIdentifier, LoginResponse, DiscoveryInformation, DiscoveryServer, DiscoveryIntegrations, DiscoveryIntegrationServer, - LoginFlow) + LoginFlow, LoginFlowList) from .crypto import UnsignedDeviceInfo, DeviceKeys, ClaimKeysResponse, QueryKeysResponse from .media import MediaRepoConfig, MXOpenGraph, OpenGraphVideo, OpenGraphImage, OpenGraphAudio from .util import (Obj, Lst, SerializerError, Serializable, SerializableEnum, SerializableAttrs, diff --git a/mautrix/types/auth.py b/mautrix/types/auth.py index 86f12201..b8d05d16 100644 --- a/mautrix/types/auth.py +++ b/mautrix/types/auth.py @@ -7,7 +7,7 @@ from attr import dataclass import attr -from .primitive import UserID, JSON +from .primitive import UserID, DeviceID, JSON from .util import SerializableAttrs, ExtensibleEnum, deserializer, Obj @@ -20,6 +20,10 @@ class LoginType(ExtensibleEnum): """ PASSWORD: 'LoginType' = "m.login.password" TOKEN: 'LoginType' = "m.login.token" + SSO: 'LoginType' = "m.login.sso" + + JWT: 'LoginType' = "org.matrix.login.jwt" + APPSERVICE: 'LoginType' = "uk.half-shot.msc2778.login.application_service" @dataclass @@ -33,6 +37,17 @@ class LoginFlow(SerializableAttrs['LoginFlow']): type: LoginType +@dataclass +class LoginFlowList(SerializableAttrs['LoginFlowList']): + flows: List[LoginFlow] + + def supports_type(self, type: LoginType) -> bool: + for flow in self.flows: + if flow.type == type: + return True + return False + + class UserIdentifierType(ExtensibleEnum): """ A user identifier type, as specified in the `Identifier types`_ section of the login spec. @@ -150,6 +165,6 @@ class LoginResponse(SerializableAttrs['LoginResponse']): https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login """ user_id: UserID - device_id: str + device_id: DeviceID access_token: str well_known: DiscoveryInformation = attr.ib(factory=DiscoveryInformation)