Philipp Hörist pushed to branch sasl2 at gajim / python-nbxmpp

Commits:

4 changed files:

Changes:

  • nbxmpp/client.py
    ... ... @@ -692,7 +692,10 @@ class Client(Observable):
    692 692
     
    
    693 693
             elif self.state == StreamState.AUTH_SUCCESSFUL:
    
    694 694
                 self._stream_authenticated = True
    
    695
    -            self._start_stream()
    
    695
    +            if self._sasl.is_sasl2():
    
    696
    +                self.state = StreamState.WAIT_FOR_FEATURES
    
    697
    +            else:
    
    698
    +                self._start_stream()
    
    696 699
     
    
    697 700
             elif self.state == StreamState.AUTH_FAILED:
    
    698 701
                 self._disconnect_with_error(StreamError.SASL,
    
    ... ... @@ -786,7 +789,7 @@ class Client(Observable):
    786 789
             self.state = StreamState.WAIT_FOR_TLS_PROCEED
    
    787 790
     
    
    788 791
         def _start_auth(self, features):
    
    789
    -        if not features.has_sasl():
    
    792
    +        if not features.has_sasl() and not features.has_sasl_2():
    
    790 793
                 self._log.error('Server does not support SASL')
    
    791 794
                 self._disconnect_with_error(StreamError.SASL,
    
    792 795
                                             'sasl-not-supported')
    

  • nbxmpp/namespaces.py
    ... ... @@ -143,6 +143,7 @@ class _Namespaces:
    143 143
         ROSTER_VER: str = 'urn:xmpp:features:rosterver'
    
    144 144
         RSM: str = 'http://jabber.org/protocol/rsm'
    
    145 145
         SASL: str = 'urn:ietf:params:xml:ns:xmpp-sasl'
    
    146
    +    SASL2: str = 'urn:xmpp:sasl:1'
    
    146 147
         SEARCH: str = 'jabber:iq:search'
    
    147 148
         SECLABEL: str = 'urn:xmpp:sec-label:0'
    
    148 149
         SECLABEL_CATALOG: str = 'urn:xmpp:sec-label:catalog:2'
    

  • nbxmpp/protocol.py
    ... ... @@ -1709,10 +1709,15 @@ class Features(Node):
    1709 1709
         def has_sasl(self):
    
    1710 1710
             return self.getTag('mechanisms', namespace=Namespace.SASL) is not None
    
    1711 1711
     
    
    1712
    +    def has_sasl_2(self):
    
    1713
    +        return self.getTag('mechanisms', namespace=Namespace.SASL2) is not None
    
    1714
    +
    
    1712 1715
         def get_mechs(self) -> set[str]:
    
    1713
    -        mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL)
    
    1716
    +        mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL2)
    
    1714 1717
             if mechanisms is None:
    
    1715
    -            return set()
    
    1718
    +            mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL)
    
    1719
    +            if mechanisms is None:
    
    1720
    +                return set()
    
    1716 1721
             mechanisms = mechanisms.getTags('mechanism')
    
    1717 1722
             return set(mech.getData() for mech in mechanisms)
    
    1718 1723
     
    

  • nbxmpp/sasl.py
    ... ... @@ -15,13 +15,15 @@
    15 15
     # You should have received a copy of the GNU General Public License
    
    16 16
     # along with this program; If not, see <http://www.gnu.org/licenses/>.
    
    17 17
     
    
    18
    +from typing import Any
    
    19
    +from typing import Optional
    
    20
    +
    
    18 21
     import os
    
    19 22
     import hmac
    
    20 23
     import binascii
    
    21 24
     import logging
    
    22 25
     import hashlib
    
    23 26
     from hashlib import pbkdf2_hmac
    
    24
    -from typing import Optional
    
    25 27
     
    
    26 28
     from nbxmpp.namespaces import Namespace
    
    27 29
     from nbxmpp.protocol import Node
    
    ... ... @@ -66,6 +68,8 @@ class SASL:
    66 68
     
    
    67 69
             self._allowed_mechs = None
    
    68 70
             self._enabled_mechs = None
    
    71
    +        self._sasl_ns = None
    
    72
    +        self._mechanism = None
    
    69 73
             self._error = None
    
    70 74
     
    
    71 75
             self._log = LogAdapter(log, {'context': client.log_context})
    
    ... ... @@ -74,6 +78,10 @@ class SASL:
    74 78
         def error(self):
    
    75 79
             return self._error
    
    76 80
     
    
    81
    +    def is_sasl2(self) -> bool:
    
    82
    +        assert self._sasl_ns is not None
    
    83
    +        return self._sasl_ns == Namespace.SASL2
    
    84
    +
    
    77 85
         def set_password(self, password):
    
    78 86
             self._password = password
    
    79 87
     
    
    ... ... @@ -82,8 +90,9 @@ class SASL:
    82 90
             return self._password
    
    83 91
     
    
    84 92
         def delegate(self, stanza):
    
    85
    -        if stanza.getNamespace() != Namespace.SASL:
    
    93
    +        if stanza.getNamespace() != self._sasl_ns:
    
    86 94
                 return
    
    95
    +
    
    87 96
             if stanza.getName() == 'challenge':
    
    88 97
                 self._on_challenge(stanza)
    
    89 98
             elif stanza.getName() == 'failure':
    
    ... ... @@ -95,6 +104,11 @@ class SASL:
    95 104
             self._allowed_mechs = self._client.mechs
    
    96 105
             self._enabled_mechs = self._allowed_mechs
    
    97 106
             self._mechanism = None
    
    107
    +
    
    108
    +        self._sasl_ns = Namespace.SASL
    
    109
    +        if features.has_sasl_2():
    
    110
    +            self._sasl_ns = Namespace.SASL2
    
    111
    +
    
    98 112
             self._error = None
    
    99 113
     
    
    100 114
             # -PLUS variants need TLS channel binding data
    
    ... ... @@ -153,15 +167,13 @@ class SASL:
    153 167
                 return
    
    154 168
     
    
    155 169
         def _send_initiate(self) -> None:
    
    170
    +        assert self._mechanism is not None
    
    156 171
             data = self._mechanism.get_initiate_data()
    
    157
    -        node = Node('auth',
    
    158
    -                    attrs={'xmlns': Namespace.SASL,
    
    159
    -                           'mechanism': self._mechanism.name})
    
    160
    -        if data is not None:
    
    161
    -            node.setData(data)
    
    162
    -        self._client.send_nonza(node)
    
    172
    +        nonza = get_initiate_nonza(self._sasl_ns, self._mechanism.name, data)
    
    173
    +        self._client.send_nonza(nonza)
    
    163 174
     
    
    164 175
         def _on_challenge(self, stanza) -> None:
    
    176
    +        assert self._mechanism is not None
    
    165 177
             try:
    
    166 178
                 data = self._mechanism.get_response_data(stanza.getData())
    
    167 179
             except AttributeError:
    
    ... ... @@ -174,22 +186,21 @@ class SASL:
    174 186
                 self._abort_auth()
    
    175 187
                 return
    
    176 188
     
    
    177
    -        node = Node('response',
    
    178
    -                    attrs={'xmlns': Namespace.SASL},
    
    179
    -                    payload=[data])
    
    180
    -        self._client.send_nonza(node)
    
    189
    +        nonza = get_response_nonza(self._sasl_ns, data)
    
    190
    +        self._client.send_nonza(nonza)
    
    181 191
     
    
    182 192
         def _on_success(self, stanza):
    
    183 193
             self._log.info('Successfully authenticated with remote server')
    
    194
    +        data = get_success_data(stanza, self._sasl_ns)
    
    184 195
             try:
    
    185
    -            self._mechanism.get_success_data(stanza.getData())
    
    186
    -        except AttributeError:
    
    187
    -            pass
    
    188
    -        except AuthFail as error:
    
    189
    -            self._log.error(error)
    
    196
    +            self._mechanism.validate_success_data(data)
    
    197
    +        except Exception as error:
    
    198
    +            self._log.error('Unable to validate success data: %s', error)
    
    190 199
                 self._abort_auth()
    
    191 200
                 return
    
    192 201
     
    
    202
    +        self._log.info('Validated success data')
    
    203
    +
    
    193 204
             self._on_sasl_finished(True, None, None)
    
    194 205
     
    
    195 206
         def _on_failure(self, stanza):
    
    ... ... @@ -208,7 +219,7 @@ class SASL:
    208 219
             self._abort_auth(reason, text)
    
    209 220
     
    
    210 221
         def _abort_auth(self, reason='malformed-request', text=None):
    
    211
    -        node = Node('abort', attrs={'xmlns': Namespace.SASL})
    
    222
    +        node = Node('abort', attrs={'xmlns': self._sasl_ns})
    
    212 223
             self._client.send_nonza(node)
    
    213 224
             self._on_sasl_finished(False, reason, text)
    
    214 225
     
    
    ... ... @@ -220,6 +231,33 @@ class SASL:
    220 231
                 self._client.set_state(StreamState.AUTH_SUCCESSFUL)
    
    221 232
     
    
    222 233
     
    
    234
    +def get_initiate_nonza(ns: str,
    
    235
    +                       mechanism: str,
    
    236
    +                       data: Optional[str]) -> Any:
    
    237
    +
    
    238
    +    if ns == Namespace.SASL:
    
    239
    +        node = Node('auth', attrs={'xmlns': ns, 'mechanism': mechanism})
    
    240
    +        if data is not None:
    
    241
    +            node.setData(data)
    
    242
    +
    
    243
    +    else:
    
    244
    +        node = Node('authenticate', attrs={'xmlns': ns, 'mechanism': mechanism})
    
    245
    +        if data is not None:
    
    246
    +            node.setTagData('initial-response', data)
    
    247
    +
    
    248
    +    return node
    
    249
    +
    
    250
    +
    
    251
    +def get_response_nonza(ns: str, data: str) -> Any:
    
    252
    +    return Node('response', attrs={'xmlns': ns}, payload=[data])
    
    253
    +
    
    254
    +
    
    255
    +def get_success_data(stanza: Any, ns: str) -> Optional[str]:
    
    256
    +    if ns == Namespace.SASL2:
    
    257
    +        return stanza.getTagData('additional-data')
    
    258
    +    return stanza.getData()
    
    259
    +
    
    260
    +
    
    223 261
     class BaseMechanism:
    
    224 262
     
    
    225 263
         name: str
    
    ... ... @@ -235,8 +273,8 @@ class BaseMechanism:
    235 273
         def get_response_data(self, data: str) -> str:
    
    236 274
             raise NotImplementedError
    
    237 275
     
    
    238
    -    def validate_success_data(self, data: str) -> None:
    
    239
    -        raise NotImplementedError
    
    276
    +    def validate_success_data(self, _data: str) -> None:
    
    277
    +        return None
    
    240 278
     
    
    241 279
     
    
    242 280
     class PLAIN(BaseMechanism):