OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*- | |
2 # | |
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 """ | |
7 Protocol agnostic implementations of SASL authentication mechanisms. | |
8 """ | |
9 | |
10 import md5, binascii, random, time, os | |
11 | |
12 from zope.interface import Interface, Attribute, implements | |
13 | |
14 class ISASLMechanism(Interface): | |
15 name = Attribute("""Common name for the SASL Mechanism.""") | |
16 | |
17 def getInitialResponse(): | |
18 """ | |
19 Get the initial client response, if defined for this mechanism. | |
20 | |
21 @return: initial client response string. | |
22 @rtype: L{str}. | |
23 """ | |
24 | |
25 | |
26 def getResponse(challenge): | |
27 """ | |
28 Get the response to a server challenge. | |
29 | |
30 @param challenge: server challenge. | |
31 @type challenge: L{str}. | |
32 @return: client response. | |
33 @rtype: L{str}. | |
34 """ | |
35 | |
36 | |
37 | |
38 class Plain(object): | |
39 """ | |
40 Implements the PLAIN SASL authentication mechanism. | |
41 | |
42 The PLAIN SASL authentication mechanism is defined in RFC 2595. | |
43 """ | |
44 implements(ISASLMechanism) | |
45 | |
46 name = 'PLAIN' | |
47 | |
48 def __init__(self, authzid, authcid, password): | |
49 self.authzid = authzid or '' | |
50 self.authcid = authcid or '' | |
51 self.password = password or '' | |
52 | |
53 | |
54 def getInitialResponse(self): | |
55 return "%s\x00%s\x00%s" % (self.authzid.encode('utf-8'), | |
56 self.authcid.encode('utf-8'), | |
57 self.password.encode('utf-8')) | |
58 | |
59 | |
60 | |
61 class DigestMD5(object): | |
62 """ | |
63 Implements the DIGEST-MD5 SASL authentication mechanism. | |
64 | |
65 The DIGEST-MD5 SASL authentication mechanism is defined in RFC 2831. | |
66 """ | |
67 implements(ISASLMechanism) | |
68 | |
69 name = 'DIGEST-MD5' | |
70 | |
71 def __init__(self, serv_type, host, serv_name, username, password): | |
72 self.username = username | |
73 self.password = password | |
74 self.defaultRealm = host | |
75 | |
76 self.digest_uri = '%s/%s' % (serv_type, host) | |
77 if serv_name is not None: | |
78 self.digest_uri += '/%s' % serv_name | |
79 | |
80 | |
81 def getInitialResponse(self): | |
82 return None | |
83 | |
84 | |
85 def getResponse(self, challenge): | |
86 directives = self._parse(challenge) | |
87 | |
88 # Compat for implementations that do not send this along with | |
89 # a succesful authentication. | |
90 if 'rspauth' in directives: | |
91 return '' | |
92 | |
93 try: | |
94 realm = directives['realm'] | |
95 except KeyError: | |
96 realm = self.defaultRealm | |
97 | |
98 return self._gen_response(directives['charset'], | |
99 realm, | |
100 directives['nonce']) | |
101 | |
102 def _parse(self, challenge): | |
103 """ | |
104 Parses the server challenge. | |
105 | |
106 Splits the challenge into a dictionary of directives with values. | |
107 | |
108 @return: challenge directives and their values. | |
109 @rtype: L{dict} of L{str} to L{str}. | |
110 """ | |
111 s = challenge | |
112 paramDict = {} | |
113 cur = 0 | |
114 remainingParams = True | |
115 while remainingParams: | |
116 # Parse a param. We can't just split on commas, because there can | |
117 # be some commas inside (quoted) param values, e.g.: | |
118 # qop="auth,auth-int" | |
119 | |
120 middle = s.index("=", cur) | |
121 name = s[cur:middle].lstrip() | |
122 middle += 1 | |
123 if s[middle] == '"': | |
124 middle += 1 | |
125 end = s.index('"', middle) | |
126 value = s[middle:end] | |
127 cur = s.find(',', end) + 1 | |
128 if cur == 0: | |
129 remainingParams = False | |
130 else: | |
131 end = s.find(',', middle) | |
132 if end == -1: | |
133 value = s[middle:].rstrip() | |
134 remainingParams = False | |
135 else: | |
136 value = s[middle:end].rstrip() | |
137 cur = end + 1 | |
138 paramDict[name] = value | |
139 | |
140 for param in ('qop', 'cipher'): | |
141 if param in paramDict: | |
142 paramDict[param] = paramDict[param].split(',') | |
143 | |
144 return paramDict | |
145 | |
146 def _unparse(self, directives): | |
147 """ | |
148 Create message string from directives. | |
149 | |
150 @param directives: dictionary of directives (names to their values). | |
151 For certain directives, extra quotes are added, as | |
152 needed. | |
153 @type directives: L{dict} of L{str} to L{str} | |
154 @return: message string. | |
155 @rtype: L{str}. | |
156 """ | |
157 | |
158 directive_list = [] | |
159 for name, value in directives.iteritems(): | |
160 if name in ('username', 'realm', 'cnonce', | |
161 'nonce', 'digest-uri', 'authzid', 'cipher'): | |
162 directive = '%s="%s"' % (name, value) | |
163 else: | |
164 directive = '%s=%s' % (name, value) | |
165 | |
166 directive_list.append(directive) | |
167 | |
168 return ','.join(directive_list) | |
169 | |
170 | |
171 def _gen_response(self, charset, realm, nonce): | |
172 """ | |
173 Generate response-value. | |
174 | |
175 Creates a response to a challenge according to section 2.1.2.1 of | |
176 RFC 2831 using the L{charset}, L{realm} and L{nonce} directives | |
177 from the challenge. | |
178 """ | |
179 | |
180 def H(s): | |
181 return md5.new(s).digest() | |
182 | |
183 def HEX(n): | |
184 return binascii.b2a_hex(n) | |
185 | |
186 def KD(k, s): | |
187 return H('%s:%s' % (k, s)) | |
188 | |
189 try: | |
190 username = self.username.encode(charset) | |
191 password = self.password.encode(charset) | |
192 except UnicodeError: | |
193 # TODO - add error checking | |
194 raise | |
195 | |
196 nc = '%08x' % 1 # TODO: support subsequent auth. | |
197 cnonce = self._gen_nonce() | |
198 qop = 'auth' | |
199 | |
200 # TODO - add support for authzid | |
201 a1 = "%s:%s:%s" % (H("%s:%s:%s" % (username, realm, password)), | |
202 nonce, | |
203 cnonce) | |
204 a2 = "AUTHENTICATE:%s" % self.digest_uri | |
205 | |
206 response = HEX( KD ( HEX(H(a1)), | |
207 "%s:%s:%s:%s:%s" % (nonce, nc, | |
208 cnonce, "auth", HEX(H(a2))))) | |
209 | |
210 directives = {'username': username, | |
211 'realm' : realm, | |
212 'nonce' : nonce, | |
213 'cnonce' : cnonce, | |
214 'nc' : nc, | |
215 'qop' : qop, | |
216 'digest-uri': self.digest_uri, | |
217 'response': response, | |
218 'charset': charset} | |
219 | |
220 return self._unparse(directives) | |
221 | |
222 | |
223 def _gen_nonce(self): | |
224 return md5.new("%s:%s:%s" % (str(random.random()) , str(time.gmtime()),s
tr(os.getpid()))).hexdigest() | |
OLD | NEW |