Skip to content

Commit 9bb3057

Browse files
author
Ben Weaver
committed
Bugfixes after some testing against Adium through python-xmpp-server.
1 parent 1be3d69 commit 9bb3057

File tree

3 files changed

+64
-39
lines changed

3 files changed

+64
-39
lines changed

sasl/digest_md5.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class DigestMD5(mech.Mechanism):
1919
def __init__(self, auth):
2020
self.auth = auth
2121

22+
state = mech.AuthState
23+
2224
## Server
2325
## 1. Issue challenge.
2426
## 2. Verify challenge response is correct; reply with rspauth.
@@ -27,56 +29,56 @@ def __init__(self, auth):
2729
def challenge(self):
2830
## Issue a challenge; continue by verifying the client's
2931
## response.
30-
return (self.verify_challenge, self.write(self.CHALLENGE, {
32+
return self.state(self.verify, None, self.write(self.CHALLENGE, {
3133
'realm': self.auth.realm(),
3234
'nonce': self.make_nonce(),
3335
'charset': 'utf-8',
3436
'algorithm': 'md5-sess'
3537
}))
3638

37-
def verify_challenge(self, data):
39+
def verify(self, entity, data):
3840
try:
3941
response = dict(rfc.data(self.RESPOND, data))
4042
except rfc.ReadError as exc:
41-
return (False, None)
43+
return self.state(False, entity, None)
4244

4345
## If the nonce count is not one, the client is trying to use
4446
## "subsequent authentication", but this is usupported.
4547
if response.get('nc') != 1:
46-
return (False, '')
48+
return self.state(False, entity, '')
4749

4850
## Confirm the digest-uri.
4951
if response.get('digest-uri') != self.make_digest_uri():
50-
return (False, '')
52+
return self.state(False, entity, '')
5153

5254
## Make sure the username exists, get the stored password.
53-
username = response.get('username')
54-
passwd = username and self.auth.get_password(username)
55+
entity = response.get('username')
56+
passwd = entity and self.auth.get_password(entity)
5557
if not passwd:
56-
return (False, '')
58+
return self.state(False, entity, '')
5759

5860
## The password must be stored in a format compatible with
5961
## DigestMD5Password. Convert it to a binary representation
6062
## and generate the response hashes.
6163
try:
62-
uh = DigestMD5Password.digest(self.auth, username, passwd)
64+
uh = DigestMD5Password.digest(self.auth, entity, passwd)
6365
(expect, rspauth) = self.make_response(response, uh)
6466
except auth.PasswordError as exc:
65-
return (False, '')
67+
return self.state(False, entity, '')
6668

6769
## Verify that the response hashes match.
6870
if expect != response.get('response'):
69-
return (False, '')
71+
return self.state(False, entity, '')
7072

7173
## Return the rspauth hash; wait for acknowledgement.
72-
return (self.receive_ack, self.write(self.VERIFY, {
74+
return self.state(self.finish, entity, self.write(self.VERIFY, {
7375
'rspauth': rspauth
7476
}))
7577

76-
def receive_ack(self, data):
78+
def finish(self, entity, data):
7779
## The client has acknowledges the rspauth sent;
7880
## authentication was successful.
79-
return (True, '')
81+
return self.state(True, entity, '')
8082

8183
## Client
8284
## 1. Respond to server challenge.
@@ -87,15 +89,16 @@ def respond(self, data):
8789
try:
8890
challenge = dict(rfc.data(self.CHALLENGE, data))
8991
except rfc.ReadError:
90-
return (False, None)
92+
return self.state(False, None, None)
9193

9294
enc = self.encoding(challenge)
9395
zid = self.auth.authorization_id()
96+
cid = unicode(self.auth.username()).encode(enc)
9497

9598
## Derive response parameters from the challenge and from the
9699
## client environment.
97100
params = {
98-
'username': unicode(self.auth.username()).encode(enc),
101+
'username': cid,
99102
'realm': challenge.get('realm', u''),
100103
'nonce': challenge['nonce'],
101104
'cnonce': self.make_nonce(),
@@ -112,23 +115,25 @@ def respond(self, data):
112115

113116
## Generate the response; continue by acknowledging the
114117
## server's response.
115-
ack = lambda d: self.acknowledge(expect, d)
116-
return (ack, self.write(self.RESPOND, params))
118+
ack = lambda *a: self.acknowledge(expect, *a)
119+
return self.state(ack, zid or cid, self.write(self.RESPOND, params))
117120

118-
def acknowledge(self, expect, data):
121+
def acknowledge(self, expect, entity, data):
119122
try:
120123
verify = dict(rfc.data(self.VERIFY, data))
121124
except rfc.ReadError:
122-
return (False, None)
125+
return self.state(False, entity, None)
123126

124127
## If the rspauth matches the expected value, authentication
125128
## was successful. The server expects an empty reply that
126129
## confirms acknowledgement.
127-
return (verify.get('rspauth') == expect and self.ack_accepted, '')
130+
if verify.get('rspauth') == expect:
131+
return self.state(self.accepted, entity, '')
132+
return self.state(False, entity, None)
128133

129-
def ack_accepted(self, data):
134+
def accepted(self, entity, data):
130135
assert data == ''
131-
return (True, None)
136+
return self.state(True, entity, None)
132137

133138
## Grammars
134139

@@ -181,7 +186,7 @@ def make_digest_uri(self):
181186
service = auth.service_name()
182187
return '%s/%s%s' % (
183188
auth.service_type(),
184-
auth.host(),
189+
auth.realm(),
185190
('/%s' % service if service else u'')
186191
)
187192

@@ -247,7 +252,7 @@ def user_hash(user, realm, passwd, encoding='utf-8'):
247252
realm = iso_8859_1(realm)
248253
passwd = iso_8859_1(passwd)
249254

250-
return colons(md5, user, passwd, realm)
255+
return colons(md5, user, realm, passwd)
251256

252257
def a1_hash(uh, nonce, cnonce, authzid):
253258
"""A1 hash (see RFC-2831 page 10). The uh parameter is the result

sasl/mechanism.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<http://www.iana.org/assignments/sasl-mechanisms>
1313
"""
1414
from __future__ import absolute_import
15-
import abc, re
15+
import abc, re, collections
1616

1717
__all__ = ('define', 'Mechanism')
1818

@@ -32,14 +32,13 @@ def __new__(mcls, name, bases, attr):
3232

3333
class Mechanism(object):
3434
"""The SASL mechanism interface. The only two methods required
35-
are challenge() and respond(). These methods return
36-
<continuation, data> items. The continuation is one of the
37-
following:
35+
are challenge() and respond(). These methods return AuthState
36+
items. The continuation field (k) is one of the following:
3837
3938
callback call this with data received from the other end
4039
True authentication succeeded
4140
False authentication failed
42-
None success/failure is up to the other end
41+
None need confirmation of success from other end
4342
4443
A Mechanism can implement any number of steps in an authentication
4544
sequence by returning callback procedures as the continuation.
@@ -59,6 +58,20 @@ def challenge(self):
5958
def respond(self, challenge):
6059
"""Respond to a challenge."""
6160

61+
class AuthState(collections.namedtuple('AuthState', 'k entity data')):
62+
63+
def __call__(self, response):
64+
return self.k and self.k(self.entity, response)
65+
66+
def success(self):
67+
return self.k is True
68+
69+
def failure(self):
70+
return self.k is False
71+
72+
def confirm(self):
73+
return self.k is None
74+
6275

6376
### Registry
6477

sasl/plain.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,32 @@ class Plain(mech.Mechanism):
1515
id, the authentication id, and password separated by null
1616
bytes."""
1717

18-
NULL = u'0x00'
18+
NULL = u'\x00'
1919

2020
def __init__(self, auth):
2121
self.auth = auth
2222

23+
def verify(self, *args):
24+
return self.auth.verify_password(*args)
25+
26+
state = mech.AuthState
27+
2328
## Server
2429

2530
def challenge(self):
26-
return (self.verify_challenge, '')
31+
return self.state(self.verify_challenge, None, '')
2732

28-
def verify_challenge(self, response):
33+
def verify_challenge(self, entity, response):
2934
try:
3035
(zid, cid, passwd) = response.decode('utf-8').split(self.NULL)
31-
except ValueError:
32-
return (False, None)
36+
except ValueError as exc:
37+
return self.state(False, entity, None)
3338

3439
try:
35-
return (self.auth.verify_password(zid, cid, passwd), None)
36-
except auth.PasswordError:
37-
return (False, None)
40+
entity = entity or zid or cid
41+
return self.state(self.verify(zid, cid, passwd), entity, None)
42+
except auth.PasswordError as exc:
43+
return self.state(False, entity, None)
3844

3945
## Client
4046

@@ -51,7 +57,8 @@ def respond(self, data):
5157
(auth.password() or u'')
5258
)).encode('utf-8')
5359

54-
return (None, response)
60+
self.authorized = zid or cid
61+
return self.state(None, zid or cid, response)
5562

5663
class PlainPassword(auth.PasswordType):
5764

0 commit comments

Comments
 (0)