diff --git a/.env.sample b/.env.sample index d09e311..ccb5956 100644 --- a/.env.sample +++ b/.env.sample @@ -111,3 +111,7 @@ ALLOWED_LIFETIME_MAX=4w #SECRET_SIGNAL_AS_TOKEN_VERSION=v1 #SECRET_SIGNAL_DB_PASSWORD_VERSION=v1 #SECRET_SIGNAL_HS_TOKEN_VERSION=v1 + +#COMPOSE_FILE="$COMPOSE_FILE:compose.shared_secret_auth.yml" +#SHARED_SECRET_AUTH_ENABLED=1 +#SECRET_SHARED_SECRET_AUTH_VERSION=v1 # length=128 diff --git a/abra.sh b/abra.sh index 3d37419..e2934cc 100644 --- a/abra.sh +++ b/abra.sh @@ -1,6 +1,7 @@ export ENTRYPOINT_CONF_VERSION=v1 -export HOMESERVER_YAML_VERSION=v12 +export HOMESERVER_YAML_VERSION=v13 export LOG_CONFIG_VERSION=v2 -export TELEGRAM_BRIDGE_YAML_VERSION=v2 +export TELEGRAM_BRIDGE_YAML_VERSION=v3 export DISCORD_BRIDGE_YAML_VERSION=v1 -export SIGNAL_BRIDGE_YAML_VERSION=v1 +export SIGNAL_BRIDGE_YAML_VERSION=v2 +export SHARED_SECRET_AUTH_VERSION=v1 diff --git a/compose.shared_secret_auth.yml b/compose.shared_secret_auth.yml new file mode 100644 index 0000000..fba5ade --- /dev/null +++ b/compose.shared_secret_auth.yml @@ -0,0 +1,22 @@ +--- +version: "3.8" + +services: + app: + environment: + - SHARED_SECRET_AUTH_ENABLED + secrets: + - shared_secret_auth + configs: + - source: shared_secret_auth + target: /usr/local/lib/python3.9/site-packages/shared_secret_authenticator.py + +configs: + shared_secret_auth: + name: ${STACK_NAME}_shared_secret_auth_${SHARED_SECRET_AUTH_VERSION} + file: shared_secret_authenticator.py + +secrets: + shared_secret_auth: + external: true + name: ${STACK_NAME}_shared_secret_auth_${SECRET_SHARED_SECRET_AUTH_VERSION} diff --git a/homeserver.yaml.tmpl b/homeserver.yaml.tmpl index c52e34b..6f31030 100644 --- a/homeserver.yaml.tmpl +++ b/homeserver.yaml.tmpl @@ -20,7 +20,12 @@ modules: # do_thing: true # - module: my_other_super_module.SomeClass # config: {} - + {{ if eq (env "SHARED_SECRET_AUTH_ENABLED") "1" }} + - module: shared_secret_authenticator.SharedSecretAuthProvider + config: + shared_secret: {{ secret "shared_secret_auth" }} + m_login_password_support_enabled: true + {{ end }} ## Server ## diff --git a/shared_secret_authenticator.py b/shared_secret_authenticator.py new file mode 100644 index 0000000..c24d8dd --- /dev/null +++ b/shared_secret_authenticator.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# +# Shared Secret Authenticator module for Matrix Synapse +# Copyright (C) 2018 Slavi Pantaleev +# +# https://devture.com/ +# +# 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 Awaitable, Callable, Optional, Tuple + +import hashlib +import hmac +import logging + +import synapse +from synapse import module_api + +logger = logging.getLogger(__name__) + +class SharedSecretAuthProvider: + def __init__(self, config: dict, api: module_api): + for k in ('shared_secret',): + if k not in config: + raise KeyError('Required `{0}` configuration key not found'.format(k)) + + m_login_password_support_enabled = bool(config['m_login_password_support_enabled']) if 'm_login_password_support_enabled' in config else False + com_devture_shared_secret_auth_support_enabled = bool(config['com_devture_shared_secret_auth_support_enabled']) if 'com_devture_shared_secret_auth_support_enabled' in config else True + + self.api = api + self.shared_secret = config['shared_secret'] + + auth_checkers: Optional[Dict[Tuple[str, Tuple], CHECK_AUTH_CALLBACK]] = {} + if com_devture_shared_secret_auth_support_enabled: + auth_checkers[("com.devture.shared_secret_auth", ("token",))] = self.check_com_devture_shared_secret_auth + if m_login_password_support_enabled: + auth_checkers[("m.login.password", ("password",))] = self.check_m_login_password + + enabled_login_types = [k[0] for k in auth_checkers] + + if len(enabled_login_types) == 0: + raise RuntimeError('At least one login type must be enabled') + + logger.info('Enabled login types: %s', enabled_login_types) + + api.register_password_auth_provider_callbacks( + auth_checkers=auth_checkers, + ) + + async def check_com_devture_shared_secret_auth( + self, + username: str, + login_type: str, + login_dict: "synapse.module_api.JsonDict", + ) -> Optional[ + Tuple[ + str, + Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]], + ] + ]: + if login_type != "com.devture.shared_secret_auth": + return None + return await self._log_in_username_with_token("com.devture.shared_secret_auth", username, login_dict.get("token")) + + async def check_m_login_password( + self, + username: str, + login_type: str, + login_dict: "synapse.module_api.JsonDict", + ) -> Optional[ + Tuple[ + str, + Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]], + ] + ]: + if login_type != "m.login.password": + return None + return await self._log_in_username_with_token("m.login.password", username, login_dict.get("password")) + + async def _log_in_username_with_token( + self, + login_type: str, + username: str, + token: str, + ) -> Optional[ + Tuple[ + str, + Optional[Callable[["synapse.module_api.LoginResponse"], Awaitable[None]]], + ] + ]: + logger.info('Authenticating user `%s` with login type `%s`', username, login_type) + + full_user_id = self.api.get_qualified_user_id(username) + + # The password (token) is supposed to be an HMAC of the full user id, keyed with the shared secret. + given_hmac = token.encode('utf-8') + + h = hmac.new(self.shared_secret.encode('utf-8'), full_user_id.encode('utf-8'), hashlib.sha512) + computed_hmac = h.hexdigest().encode('utf-8') + + if not hmac.compare_digest(computed_hmac, given_hmac): + logger.info('Bad hmac value for user: %s', full_user_id) + return None + + user_info = await self.api.get_userinfo_by_id(full_user_id) + if user_info is None: + logger.info('Refusing to authenticate missing user: %s', full_user_id) + return None + + logger.info('Authenticated user: %s', full_user_id) + + return full_user_id, None diff --git a/signal_bridge.yaml.tmpl b/signal_bridge.yaml.tmpl index 055f108..c0a2e60 100644 --- a/signal_bridge.yaml.tmpl +++ b/signal_bridge.yaml.tmpl @@ -145,7 +145,7 @@ bridge: double_puppet_allow_discovery: false # Servers to allow double puppeting from, even if double_puppet_allow_discovery is false. double_puppet_server_map: - example.com: https://example.com + {{ env "HOMESERVER_DOMAIN" }}: {{ env "HOMESERVER_URL" }} # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth # # If set, custom puppets will be enabled automatically for local users @@ -154,7 +154,7 @@ bridge: # If using this for other servers than the bridge's server, # you must also set the URL in the double_puppet_server_map. login_shared_secret_map: - example.com: foo + {{ env "HOMESERVER_DOMAIN" }}: {{ secret "shared_secret_auth" }} # Whether or not created rooms should have federation enabled. # If false, created portal rooms will never be federated. federate_rooms: true diff --git a/telegram_bridge.yaml.tmpl b/telegram_bridge.yaml.tmpl index dfe7e30..db350d5 100644 --- a/telegram_bridge.yaml.tmpl +++ b/telegram_bridge.yaml.tmpl @@ -189,7 +189,7 @@ bridge: sync_direct_chat_list: false # Servers to always allow double puppeting from double_puppet_server_map: - example.com: https://example.com + {{ env "HOMESERVER_DOMAIN" }}: {{ env "HOMESERVER_URL" }} # Allow using double puppeting from any server with a valid client .well-known file. double_puppet_allow_discovery: false # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth @@ -200,7 +200,7 @@ bridge: # If using this for other servers than the bridge's server, # you must also set the URL in the double_puppet_server_map. login_shared_secret_map: - example.com: foobar + {{ env "HOMESERVER_DOMAIN" }}: {{ secret "shared_secret_auth" }} # Set to false to disable link previews in messages sent to Telegram. telegram_link_preview: true # Whether or not the !tg join command should do a HTTP request