Commit f2dcc639 authored by Philipp Hörist's avatar Philipp Hörist

Rewrite SASL from scratch

- Move NonBlockingBind to own module
- Add SCRAM-SHA-256 and SCRAM-SHA-256-PLUS support
parent 823b7ae5
Pipeline #2729 passed with stages
in 27 seconds
......@@ -9,7 +9,7 @@ Thanks and credits to the xmpppy developers. See: http://xmpppy.sourceforge.net/
"""
from .protocol import *
from . import simplexml, protocol, auth_nb, transports_nb, roster_nb
from . import simplexml, protocol, auth, transports_nb, roster_nb
from . import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors
from .client_nb import NonBlockingClient
from .plugin import PlugIn
......
This diff is collapsed.
This diff is collapsed.
# Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
# Copyright (C) Dimitur Kirov <dkirov AT gmail.com>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
import logging
from nbxmpp.plugin import PlugIn
from nbxmpp.protocol import NS_BIND
from nbxmpp.protocol import NS_SESSION
from nbxmpp.protocol import NS_STREAMS
from nbxmpp.protocol import NS_STREAM_MGMT
from nbxmpp.protocol import Node
from nbxmpp.protocol import isResultNode
from nbxmpp.protocol import Protocol
from nbxmpp.const import Realm
from nbxmpp.const import Event
log = logging.getLogger('nbxmpp.bind')
class NonBlockingBind(PlugIn):
"""
Bind some JID to the current connection to allow router know of our
location. Must be plugged after successful SASL auth
"""
def __init__(self):
PlugIn.__init__(self)
self._session_required = False
def plugin(self, _owner):
self._owner.RegisterHandler(
'features', self._on_features, xmlns=NS_STREAMS)
def _on_features(self, _con, feats):
"""
Determine if server supports resource binding and set some internal
attributes accordingly.
"""
if not feats or not feats.getTag('bind', namespace=NS_BIND):
return
session = feats.getTag('session', namespace=NS_SESSION)
if session is not None:
if session.getTag('optional') is None:
self._session_required = True
self._bind()
def plugout(self):
"""
Remove Bind handler from owner's dispatcher. Used internally
"""
self._owner.UnregisterHandler(
'features', self._on_features, xmlns=NS_STREAMS)
def _bind(self):
"""
Perform binding. Use provided resource name or random (if not provided).
"""
resource = []
if self._owner._Resource:
resource = [Node('resource', payload=[self._owner._Resource])]
payload = Node('bind', attrs={'xmlns': NS_BIND}, payload=resource)
node = Protocol('iq', typ='set', payload=[payload])
self._owner.Dispatcher.SendAndWaitForResponse(node, func=self._on_bind)
def _on_bind(self, stanza):
if isResultNode(stanza):
bind = stanza.getTag('bind')
if bind is not None:
jid = bind.getTagData('jid')
log.info('Successfully bound %s', jid)
self._owner.set_bound_jid(jid)
if not self._session_required:
# Server don't want us to initialize a session
log.info('No session required')
self._on_bind_successful()
else:
node = Node('session', attrs={'xmlns':NS_SESSION})
iq = Protocol('iq', typ='set', payload=[node])
self._owner.SendAndWaitForResponse(
iq, func=self._on_session)
self.PlugOut()
return
if stanza:
log.error('Binding failed: %s.', stanza.getTag('error'))
else:
log.error('Binding failed: timeout expired')
self._owner.Connection.start_disconnect()
self._owner.Dispatcher.Event(Realm.CONNECTING, Event.BIND_FAILED)
self.PlugOut()
def _on_session(self, stanza):
if isResultNode(stanza):
log.info('Successfully started session')
self._on_bind_successful()
else:
log.error('Session open failed')
self._owner.Connection.start_disconnect()
self._owner.Dispatcher.Event(Realm.CONNECTING, Event.SESSION_FAILED)
self.PlugOut()
def _on_bind_successful(self):
feats = self._owner.Dispatcher.Stream.features
if feats.getTag('sm', namespace=NS_STREAM_MGMT):
self._owner.Smacks.send_enable()
self._owner.Dispatcher.Event(Realm.CONNECTING, Event.CONNECTION_ACTIVE)
......@@ -21,11 +21,11 @@ Client class establishes connection to XMPP Server and handles authentication
import socket
import logging
from . import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh
from . import transports_nb, dispatcher_nb, roster_nb, protocol, bosh
from .protocol import NS_TLS
from .protocol import JID
from .auth_nb import SASL_AUTHENTICATION_MECHANISMS
from .auth_nb import NonBlockingBind
from .auth import SASL
from .bind import NonBlockingBind
from .smacks import Smacks
from .const import Realm
from .const import Event
......@@ -135,13 +135,7 @@ class NonBlockingClient(object):
if 'NonBlockingRoster' in self.__dict__:
self.NonBlockingRoster.PlugOut()
if 'SASL' in self.__dict__:
if 'startsasl' in self.SASL.__dict__ and \
self.SASL.startsasl == 'failure-in-process':
sasl_failed = True
self.SASL.startsasl = 'failure'
self._on_start_sasl()
else:
self.SASL.PlugOut()
self.SASL.PlugOut()
if 'NonBlockingTCP' in self.__dict__:
self.NonBlockingTCP.PlugOut()
if 'NonBlockingHTTP' in self.__dict__:
......@@ -521,7 +515,7 @@ class NonBlockingClient(object):
### follows code for authentication, resource bind, session and roster download
###############################################################################
def auth(self, user, password, resource='', sasl=True, auth_mechs=None):
def auth(self, user, password, resource='', auth_mechs=None):
"""
Authenticate connnection and bind resource. If resource is not provided
random one or library name used
......@@ -533,66 +527,43 @@ class NonBlockingClient(object):
:param auth_mechs: Set of valid authentification mechanisms. If None all
authentification mechanisms will be allowed. Possible entries are:
'ANONYMOUS', 'EXTERNAL', 'GSSAPI', 'SCRAM-SHA-1-PLUS',
'SCRAM-SHA-1', 'DIGEST-MD5', 'PLAIN', 'X-MESSENGER-OAUTH2',
'XEP-0078'
"""
self._User, self._Password = user, password
self._Resource, self._sasl = resource, sasl
self._channel_binding = None
if self.connected in ('ssl', 'tls'):
try:
if self.protocol_type != 'BOSH':
self._channel_binding = self.Connection.NonBlockingTLS.get_channel_binding()
# TLS handshake is finished so channel binding data muss exist
assert (self._channel_binding is not None)
except NotImplementedError:
pass
if auth_mechs is None:
self._auth_mechs = SASL_AUTHENTICATION_MECHANISMS
else:
self._auth_mechs = auth_mechs
self._on_doc_attrs()
def _on_doc_attrs(self):
"""
Plug authentication objects and start auth
'SCRAM-SHA-1', 'PLAIN'
"""
if self._sasl:
auth_nb.SASL.get_instance(self._User, self._Password,
self._on_start_sasl, self._channel_binding,
self._auth_mechs).PlugIn(self)
if not hasattr(self, 'SASL'):
if 'SASL' in self.__dict__:
log.error('Auth not possible while another auth is in progress')
return
self.SASL.auth()
return True
self._User = user
self._Password = password
self._Resource = resource
def _on_start_sasl(self, data=None):
"""
Callback used by SASL, called on each auth step
"""
if self.SASL.startsasl == 'in-process':
return
self.onreceive(None)
SASL.get_instance(self._User,
self._Password,
auth_mechs,
self._on_sasl_finished).PlugIn(self)
def _on_sasl_finished(self, success, reason, text):
if success:
self.SASL.PlugOut()
if self.protocol_type == 'BOSH':
self.Dispatcher.after_SASL = True
self.Dispatcher.StreamInit()
if self.SASL.startsasl == 'failure':
# wrong user/pass, stop auth
if 'SASL' in self.__dict__:
self.SASL.PlugOut()
self.connected = None # FIXME: is this intended? We use ''elsewhere
self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_FAILED, data)
elif self.SASL.startsasl == 'success':
self.connected += '+sasl'
self.Dispatcher.Event(Realm.CONNECTING, Event.AUTH_SUCCESSFUL)
self.Dispatcher.Event(Realm.CONNECTING,
Event.AUTH_SUCCESSFUL)
else:
self.Dispatcher.Event(Realm.CONNECTING,
Event.AUTH_FAILED,
(reason, text))
def bind(self):
self._owner.Smacks.register_handlers()
self.Smacks.register_handlers()
# Check if we can resume
if self._owner.Smacks.resume_supported:
self._owner.Smacks.resume_request()
if self.Smacks.resume_supported:
self.Smacks.resume_request()
else:
# If we cant resume we bind and enable sm afterwards
NonBlockingBind.get_instance().PlugIn(self)
......
......@@ -16,6 +16,7 @@
# along with this program; If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
from enum import IntEnum
from enum import unique
@unique
......@@ -38,3 +39,7 @@ class Event(Enum):
def __str__(self):
return self.value
class GSSAPIState(IntEnum):
STEP = 0
WRAP = 1
......@@ -267,6 +267,17 @@ NS_OPENPGP = 'urn:xmpp:openpgp:0'
#for (k, v) in loc.items():
#print('%s = \'%s\'' % (k, v))
SASL_AUTH_MECHS = [
'SCRAM-SHA-256-PLUS',
'SCRAM-SHA-256',
'SCRAM-SHA-1-PLUS',
'SCRAM-SHA-1',
'PLAIN',
'GSSAPI',
'EXTERNAL',
'ANONYMOUS',
]
SASL_ERROR_CONDITIONS = [
'aborted',
'account-disabled',
......
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
import base64
def b64decode(data, return_type=str):
if isinstance(data, str):
data = data.encode()
result = base64.b64decode(data)
if return_type == bytes:
return result
return result.decode()
def b64encode(data, return_type=str):
if isinstance(data, str):
data = data.encode()
result = base64.b64encode(data)
if return_type == bytes:
return result
return result.decode()
import unittest
from unittest.mock import Mock
from nbxmpp.auth import SCRAM_SHA_1
class SCRAM(unittest.TestCase):
def setUp(self):
self.con = Mock()
self._method = SCRAM_SHA_1(self.con, None)
self._method._client_nonce = '4691d8f313ddb02d2eed511d5617a5c6f72efa671613c598'
self._username = 'philw'
self._password = 'testtest123'
self.auth = '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1">eSwsbj1waGlsdyxyPTQ2OTFkOGYzMTNkZGIwMmQyZWVkNTExZDU2MTdhNWM2ZjcyZWZhNjcxNjEzYzU5OA==</auth>'
self.challenge = 'cj00NjkxZDhmMzEzZGRiMDJkMmVlZDUxMWQ1NjE3YTVjNmY3MmVmYTY3MTYxM2M1OThDaEJpZGEyb0NJeks5S25QdGsxSUZnPT0scz1iZFkrbkRjdUhuVGFtNzgyaG9neHNnPT0saT00MDk2'
self.response = '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">Yz1lU3dzLHI9NDY5MWQ4ZjMxM2RkYjAyZDJlZWQ1MTFkNTYxN2E1YzZmNzJlZmE2NzE2MTNjNTk4Q2hCaWRhMm9DSXpLOUtuUHRrMUlGZz09LHA9NUd5a09hWCtSWlllR3E2L2U3YTE2UDVBeFVrPQ==</response>'
self.success = 'dj1qMGtuNlVvT1FjTmJ0MGFlYnEwV1QzYWNkSW89'
def test_auth(self):
self._method.initiate(self._username)
self.assertEqual(self.auth, str(self.con.send.call_args[0][0]))
self._method.response(self.challenge, self._password)
self.assertEqual(self.response, str(self.con.send.call_args[0][0]))
self._method.success(self.success)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment