| ... |
... |
@@ -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):
|