diff options
Diffstat (limited to 'sleekxmpp/features/feature_mechanisms/mechanisms.py')
-rw-r--r-- | sleekxmpp/features/feature_mechanisms/mechanisms.py | 179 |
1 files changed, 120 insertions, 59 deletions
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 2ab7b0a4..0132765f 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -6,12 +6,11 @@ See the file LICENSE for copying permission. """ +import sys import logging -from sleekxmpp.thirdparty import suelta -from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError -from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure - +from sleekxmpp.util import sasl +from sleekxmpp.util.stringprep_profiles import StringPrepError from sleekxmpp.stanza import StreamFeatures from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin from sleekxmpp.plugins import BasePlugin @@ -31,7 +30,15 @@ class FeatureMechanisms(BasePlugin): stanza = stanza default_config = { 'use_mech': None, + 'use_mechs': None, + 'min_mech': None, 'sasl_callback': None, + 'security_callback': None, + 'encrypted_plain': True, + 'unencrypted_plain': False, + 'unencrypted_digest': False, + 'unencrypted_cram': False, + 'unencrypted_scram': True, 'order': 100 } @@ -39,34 +46,13 @@ class FeatureMechanisms(BasePlugin): if not self.use_mech and not self.xmpp.boundjid.user: self.use_mech = 'ANONYMOUS' - def tls_active(): - return 'starttls' in self.xmpp.features - - def basic_callback(mech, values): - creds = self.xmpp.credentials - for value in values: - if value == 'username': - values['username'] = self.xmpp.boundjid.user - elif value == 'password': - values['password'] = creds['password'] - elif value == 'email': - jid = self.xmpp.boundjid.bare - values['email'] = creds.get('email', jid) - elif value in creds: - values[value] = creds[value] - mech.fulfill(values) - if self.sasl_callback is None: - self.sasl_callback = basic_callback + self.sasl_callback = self._default_credentials - self.mech = None - self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp', - username=self.xmpp.boundjid.user, - sec_query=suelta.sec_query_allow, - request_values=self.sasl_callback, - tls_active=tls_active, - mech=self.use_mech) + if self.security_callback is None: + self.security_callback = self._default_security + self.mech = None self.mech_list = set() self.attempted_mechs = set() @@ -99,6 +85,44 @@ class FeatureMechanisms(BasePlugin): restart=True, order=self.order) + def _default_credentials(self, required_values, optional_values): + creds = self.xmpp.credentials + result = {} + values = required_values.union(optional_values) + for value in values: + if value == 'username': + result[value] = self.xmpp.boundjid.user + elif value == 'password': + result[value] = creds['password'] + elif value == 'email': + jid = self.xmpp.boundjid.bare + result[value] = creds.get('email', jid) + elif value == 'channel_binding': + if sys.version_info >= (3, 3): + result[value] = self.xmpp.socket.channel_binding() + else: + result[value] = None + elif value == 'host': + result[value] = self.xmpp.boundjid.domain + elif value == 'realm': + result[value] = self.xmpp.boundjid.domain + elif value == 'service-name': + result[value] = self.xmpp.address[0] + elif value == 'service': + result[value] = 'xmpp' + elif value in creds: + result[value] = creds[value] + return result + + def _default_security(self, values): + result = {} + for value in values: + if value == 'encrypted': + result[value] = 'starttls' in self.xmpp.features + else: + result[value] = self.config.get(value, False) + return result + def _handle_sasl_auth(self, features): """ Handle authenticating using SASL. @@ -111,37 +135,61 @@ class FeatureMechanisms(BasePlugin): # server has incorrectly offered it again. return False - if not self.use_mech: - self.mech_list = set(features['mechanisms']) - else: - self.mech_list = set([self.use_mech]) + enforce_limit = False + limited_mechs = self.use_mechs + + if limited_mechs is None: + limited_mechs = set() + elif limited_mechs and not isinstance(limited_mechs, set): + limited_mechs = set(limited_mechs) + enforce_limit = True + + if self.use_mech: + limited_mechs.add(self.use_mech) + enforce_limit = True + + if enforce_limit: + self.use_mechs = limited_mechs + + self.mech_list = set(features['mechanisms']) + return self._send_auth() def _send_auth(self): mech_list = self.mech_list - self.attempted_mechs - self.mech = self.sasl.choose_mechanism(mech_list) - - if mech_list and self.mech is not None: - resp = stanza.Auth(self.xmpp) - resp['mechanism'] = self.mech.name - try: - resp['value'] = self.mech.process() - except SASLCancelled: - self.attempted_mechs.add(self.mech.name) - self._send_auth() - except SASLError: - self.attempted_mechs.add(self.mech.name) - self._send_auth() - except SASLPrepFailure: - log.exception("A credential value did not pass SASLprep.") - self.xmpp.disconnect() - else: - resp.send(now=True) - else: + try: + self.mech = sasl.choose(mech_list, + self.sasl_callback, + self.security_callback, + limit=self.use_mechs, + min_mech=self.min_mech) + except sasl.SASLNoAppropriateMechanism: log.error("No appropriate login method.") self.xmpp.event("no_auth", direct=True) self.attempted_mechs = set() + return self.xmpp.disconnect() + + resp = stanza.Auth(self.xmpp) + resp['mechanism'] = self.mech.name + try: + resp['value'] = self.mech.process() + except sasl.SASLCancelled: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + except sasl.SASLFailed: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() + except StringPrepError: + log.exception("A credential value did not pass SASLprep.") self.xmpp.disconnect() + else: + resp.send(now=True) + return True def _handle_challenge(self, stanza): @@ -149,20 +197,33 @@ class FeatureMechanisms(BasePlugin): resp = self.stanza.Response(self.xmpp) try: resp['value'] = self.mech.process(stanza['value']) - except SASLCancelled: + except sasl.SASLCancelled: self.stanza.Abort(self.xmpp).send() - except SASLError: + except sasl.SASLFailed: self.stanza.Abort(self.xmpp).send() + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() else: resp.send(now=True) def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" - self.attempted_mechs = set() - self.xmpp.authenticated = True - self.xmpp.features.add('mechanisms') - self.xmpp.event('auth_success', stanza, direct=True) - raise RestartStream() + try: + final = self.mech.process(stanza['value']) + except sasl.SASLMutualAuthFailed: + log.error("Mutual authentication failed! " + \ + "A security breach is possible.") + self.attempted_mechs.add(self.mech.name) + self.xmpp.disconnect() + else: + self.attempted_mechs = set() + self.xmpp.authenticated = True + self.xmpp.features.add('mechanisms') + self.xmpp.event('auth_success', stanza, direct=True) + raise RestartStream() def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" |