From 5455502820cd13e46dd3b8a342096a4d6aebd9e7 Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Mon, 15 Nov 2021 11:21:30 +0200 Subject: [PATCH] Upgrade shared-secret-authenticator to v2 Works well, but is still affected by https://github.com/vector-im/element-web/issues/19605 --- etc/services/matrix-synapse/homeserver.yaml | 34 ++++-- etc/services/shared_secret_authenticator.py | 125 ++++++++++++++------ 2 files changed, 113 insertions(+), 46 deletions(-) diff --git a/etc/services/matrix-synapse/homeserver.yaml b/etc/services/matrix-synapse/homeserver.yaml index 7750419..7dd2ebc 100644 --- a/etc/services/matrix-synapse/homeserver.yaml +++ b/etc/services/matrix-synapse/homeserver.yaml @@ -1,5 +1,25 @@ # vim:ft=yaml +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html + +## Modules ## + +# Server admins can expand Synapse's functionality with external modules. +# +# See https://matrix-org.github.io/synapse/develop/modules.html for more +# documentation on how to configure or create custom modules for Synapse. +# +modules: + - module: shared_secret_authenticator.SharedSecretAuthProvider + config: + shared_secret: "7DXvheK1400ydCHjAymDU50FkeUedQJ2AYpitr3inLpSBIdRJN4kfS5IkGYvUptF" + m_login_password_support_enabled: true ## Server ## # The domain name of the server, with optional explicit port. @@ -1200,13 +1220,13 @@ password_config: # #bind_dn: # #bind_password: # #filter: "(objectClass=posixAccount)" -password_providers: -- module: "shared_secret_authenticator.SharedSecretAuthenticator" - config: - sharedSecret: "7DXvheK1400ydCHjAymDU50FkeUedQJ2AYpitr3inLpSBIdRJN4kfS5IkGYvUptF" -- module: "rest_auth_provider.RestAuthProvider" - config: - endpoint: "http://matrix-corporal:41080/_matrix/corporal" +# password_providers: +# - module: "shared_secret_authenticator.SharedSecretAuthenticator" +# config: +# sharedSecret: "7DXvheK1400ydCHjAymDU50FkeUedQJ2AYpitr3inLpSBIdRJN4kfS5IkGYvUptF" +# - module: "rest_auth_provider.RestAuthProvider" +# config: +# endpoint: "http://matrix-corporal:41080/_matrix/corporal" # Clients requesting push notifications can either have the body of diff --git a/etc/services/shared_secret_authenticator.py b/etc/services/shared_secret_authenticator.py index 26313f4..01cb5fa 100644 --- a/etc/services/shared_secret_authenticator.py +++ b/etc/services/shared_secret_authenticator.py @@ -3,7 +3,7 @@ # Shared Secret Authenticator module for Matrix Synapse # Copyright (C) 2018 Slavi Pantaleev # -# http://devture.com/ +# 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 @@ -18,53 +18,100 @@ # 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 logging import hashlib import hmac -from twisted.internet import defer +import logging + +import synapse +from synapse import module_api logger = logging.getLogger(__name__) -class SharedSecretAuthenticator(object): +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 + + self.api = api + self.shared_secret = config['shared_secret'] + + auth_checkers: Optional[Dict[Tuple[str, Tuple], CHECK_AUTH_CALLBACK]] = {} + 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] + logger.info('Enabled login types: %s', enabled_login_types) - def __init__(self, config, account_handler): - self.account_handler = account_handler - self.sharedSecret = config['sharedSecret'] + api.register_password_auth_provider_callbacks( + auth_checkers=auth_checkers, + ) - @defer.inlineCallbacks - def check_password(self, user_id, password): - # The password is supposed to be an HMAC of the user id, keyed with the shared secret. - # It's not really a password in this case. - given_hmac = password.encode('utf-8') + 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")) - logger.info('Authenticating user: %s', user_id) + 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")) - h = hmac.new(self.sharedSecret.encode('utf-8'), user_id.encode('utf-8'), hashlib.sha512) + 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') - try: - is_identical = hmac.compare_digest(computed_hmac, given_hmac) - except AttributeError: - # `hmac.compare_digest` is only available on Python >= 2.7.7 - # Fall back to being somewhat insecure on older versions. - is_identical = (computed_hmac == given_hmac) - - if not is_identical: - logger.info('Bad hmac value for user: %s', user_id) - defer.returnValue(False) - return - - if not (yield self.account_handler.check_user_exists(user_id)): - logger.info('Refusing to authenticate missing user: %s', user_id) - defer.returnValue(False) - return - - logger.info('Authenticated user: %s', user_id) - defer.returnValue(True) - - @staticmethod - def parse_config(config): - if 'sharedSecret' not in config: - raise Exception('Missing sharedSecret parameter for SharedSecretAuthenticator') - return config + 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