Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / cred / credentials.py
1 # -*- test-case-name: twisted.test.test_newcred-*-
2
3 # Copyright (c) Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6
7 from zope.interface import implements, Interface
8
9 import hmac, time, random
10 from twisted.python.hashlib import md5
11 from twisted.python.randbytes import secureRandom
12 from twisted.cred._digest import calcResponse, calcHA1, calcHA2
13 from twisted.cred import error
14
15 class ICredentials(Interface):
16     """
17     I check credentials.
18
19     Implementors _must_ specify which sub-interfaces of ICredentials
20     to which it conforms, using zope.interface.implements().
21     """
22
23
24
25 class IUsernameDigestHash(ICredentials):
26     """
27     This credential is used when a CredentialChecker has access to the hash
28     of the username:realm:password as in an Apache .htdigest file.
29     """
30     def checkHash(digestHash):
31         """
32         @param digestHash: The hashed username:realm:password to check against.
33
34         @return: C{True} if the credentials represented by this object match
35             the given hash, C{False} if they do not, or a L{Deferred} which
36             will be called back with one of these values.
37         """
38
39
40
41 class IUsernameHashedPassword(ICredentials):
42     """
43     I encapsulate a username and a hashed password.
44
45     This credential is used when a hashed password is received from the
46     party requesting authentication.  CredentialCheckers which check this
47     kind of credential must store the passwords in plaintext (or as
48     password-equivalent hashes) form so that they can be hashed in a manner
49     appropriate for the particular credentials class.
50
51     @type username: C{str}
52     @ivar username: The username associated with these credentials.
53     """
54
55     def checkPassword(password):
56         """
57         Validate these credentials against the correct password.
58
59         @type password: C{str}
60         @param password: The correct, plaintext password against which to
61         check.
62
63         @rtype: C{bool} or L{Deferred}
64         @return: C{True} if the credentials represented by this object match the
65             given password, C{False} if they do not, or a L{Deferred} which will
66             be called back with one of these values.
67         """
68
69
70
71 class IUsernamePassword(ICredentials):
72     """
73     I encapsulate a username and a plaintext password.
74
75     This encapsulates the case where the password received over the network
76     has been hashed with the identity function (That is, not at all).  The
77     CredentialsChecker may store the password in whatever format it desires,
78     it need only transform the stored password in a similar way before
79     performing the comparison.
80
81     @type username: C{str}
82     @ivar username: The username associated with these credentials.
83
84     @type password: C{str}
85     @ivar password: The password associated with these credentials.
86     """
87
88     def checkPassword(password):
89         """
90         Validate these credentials against the correct password.
91
92         @type password: C{str}
93         @param password: The correct, plaintext password against which to
94         check.
95
96         @rtype: C{bool} or L{Deferred}
97         @return: C{True} if the credentials represented by this object match the
98             given password, C{False} if they do not, or a L{Deferred} which will
99             be called back with one of these values.
100         """
101
102
103
104 class IAnonymous(ICredentials):
105     """
106     I am an explicitly anonymous request for access.
107     """
108
109
110
111 class DigestedCredentials(object):
112     """
113     Yet Another Simple HTTP Digest authentication scheme.
114     """
115     implements(IUsernameHashedPassword, IUsernameDigestHash)
116
117     def __init__(self, username, method, realm, fields):
118         self.username = username
119         self.method = method
120         self.realm = realm
121         self.fields = fields
122
123
124     def checkPassword(self, password):
125         """
126         Verify that the credentials represented by this object agree with the
127         given plaintext C{password} by hashing C{password} in the same way the
128         response hash represented by this object was generated and comparing
129         the results.
130         """
131         response = self.fields.get('response')
132         uri = self.fields.get('uri')
133         nonce = self.fields.get('nonce')
134         cnonce = self.fields.get('cnonce')
135         nc = self.fields.get('nc')
136         algo = self.fields.get('algorithm', 'md5').lower()
137         qop = self.fields.get('qop', 'auth')
138
139         expected = calcResponse(
140             calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
141             calcHA2(algo, self.method, uri, qop, None),
142             algo, nonce, nc, cnonce, qop)
143
144         return expected == response
145
146
147     def checkHash(self, digestHash):
148         """
149         Verify that the credentials represented by this object agree with the
150         credentials represented by the I{H(A1)} given in C{digestHash}.
151
152         @param digestHash: A precomputed H(A1) value based on the username,
153             realm, and password associate with this credentials object.
154         """
155         response = self.fields.get('response')
156         uri = self.fields.get('uri')
157         nonce = self.fields.get('nonce')
158         cnonce = self.fields.get('cnonce')
159         nc = self.fields.get('nc')
160         algo = self.fields.get('algorithm', 'md5').lower()
161         qop = self.fields.get('qop', 'auth')
162
163         expected = calcResponse(
164             calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
165             calcHA2(algo, self.method, uri, qop, None),
166             algo, nonce, nc, cnonce, qop)
167
168         return expected == response
169
170
171
172 class DigestCredentialFactory(object):
173     """
174     Support for RFC2617 HTTP Digest Authentication
175
176     @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
177         opaque should be valid.
178
179     @type privateKey: C{str}
180     @ivar privateKey: A random string used for generating the secure opaque.
181
182     @type algorithm: C{str}
183     @param algorithm: Case insensitive string specifying the hash algorithm to
184         use.  Must be either C{'md5'} or C{'sha'}.  C{'md5-sess'} is B{not}
185         supported.
186
187     @type authenticationRealm: C{str}
188     @param authenticationRealm: case sensitive string that specifies the realm
189         portion of the challenge
190     """
191
192     CHALLENGE_LIFETIME_SECS = 15 * 60    # 15 minutes
193
194     scheme = "digest"
195
196     def __init__(self, algorithm, authenticationRealm):
197         self.algorithm = algorithm
198         self.authenticationRealm = authenticationRealm
199         self.privateKey = secureRandom(12)
200
201
202     def getChallenge(self, address):
203         """
204         Generate the challenge for use in the WWW-Authenticate header.
205
206         @param address: The client address to which this challenge is being
207         sent.
208
209         @return: The C{dict} that can be used to generate a WWW-Authenticate
210             header.
211         """
212         c = self._generateNonce()
213         o = self._generateOpaque(c, address)
214
215         return {'nonce': c,
216                 'opaque': o,
217                 'qop': 'auth',
218                 'algorithm': self.algorithm,
219                 'realm': self.authenticationRealm}
220
221
222     def _generateNonce(self):
223         """
224         Create a random value suitable for use as the nonce parameter of a
225         WWW-Authenticate challenge.
226
227         @rtype: C{str}
228         """
229         return secureRandom(12).encode('hex')
230
231
232     def _getTime(self):
233         """
234         Parameterize the time based seed used in C{_generateOpaque}
235         so we can deterministically unittest it's behavior.
236         """
237         return time.time()
238
239
240     def _generateOpaque(self, nonce, clientip):
241         """
242         Generate an opaque to be returned to the client.  This is a unique
243         string that can be returned to us and verified.
244         """
245         # Now, what we do is encode the nonce, client ip and a timestamp in the
246         # opaque value with a suitable digest.
247         now = str(int(self._getTime()))
248         if clientip is None:
249             clientip = ''
250         key = "%s,%s,%s" % (nonce, clientip, now)
251         digest = md5(key + self.privateKey).hexdigest()
252         ekey = key.encode('base64')
253         return "%s-%s" % (digest, ekey.replace('\n', ''))
254
255
256     def _verifyOpaque(self, opaque, nonce, clientip):
257         """
258         Given the opaque and nonce from the request, as well as the client IP
259         that made the request, verify that the opaque was generated by us.
260         And that it's not too old.
261
262         @param opaque: The opaque value from the Digest response
263         @param nonce: The nonce value from the Digest response
264         @param clientip: The remote IP address of the client making the request
265             or C{None} if the request was submitted over a channel where this
266             does not make sense.
267
268         @return: C{True} if the opaque was successfully verified.
269
270         @raise error.LoginFailed: if C{opaque} could not be parsed or
271             contained the wrong values.
272         """
273         # First split the digest from the key
274         opaqueParts = opaque.split('-')
275         if len(opaqueParts) != 2:
276             raise error.LoginFailed('Invalid response, invalid opaque value')
277
278         if clientip is None:
279             clientip = ''
280
281         # Verify the key
282         key = opaqueParts[1].decode('base64')
283         keyParts = key.split(',')
284
285         if len(keyParts) != 3:
286             raise error.LoginFailed('Invalid response, invalid opaque value')
287
288         if keyParts[0] != nonce:
289             raise error.LoginFailed(
290                 'Invalid response, incompatible opaque/nonce values')
291
292         if keyParts[1] != clientip:
293             raise error.LoginFailed(
294                 'Invalid response, incompatible opaque/client values')
295
296         try:
297             when = int(keyParts[2])
298         except ValueError:
299             raise error.LoginFailed(
300                 'Invalid response, invalid opaque/time values')
301
302         if (int(self._getTime()) - when >
303             DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
304
305             raise error.LoginFailed(
306                 'Invalid response, incompatible opaque/nonce too old')
307
308         # Verify the digest
309         digest = md5(key + self.privateKey).hexdigest()
310         if digest != opaqueParts[0]:
311             raise error.LoginFailed('Invalid response, invalid opaque value')
312
313         return True
314
315
316     def decode(self, response, method, host):
317         """
318         Decode the given response and attempt to generate a
319         L{DigestedCredentials} from it.
320
321         @type response: C{str}
322         @param response: A string of comma seperated key=value pairs
323
324         @type method: C{str}
325         @param method: The action requested to which this response is addressed
326         (GET, POST, INVITE, OPTIONS, etc).
327
328         @type host: C{str}
329         @param host: The address the request was sent from.
330
331         @raise error.LoginFailed: If the response does not contain a username,
332             a nonce, an opaque, or if the opaque is invalid.
333
334         @return: L{DigestedCredentials}
335         """
336         def unq(s):
337             if s[0] == s[-1] == '"':
338                 return s[1:-1]
339             return s
340         response = ' '.join(response.splitlines())
341         parts = response.split(',')
342
343         auth = {}
344
345         for (k, v) in [p.split('=', 1) for p in parts]:
346             auth[k.strip()] = unq(v.strip())
347
348         username = auth.get('username')
349         if not username:
350             raise error.LoginFailed('Invalid response, no username given.')
351
352         if 'opaque' not in auth:
353             raise error.LoginFailed('Invalid response, no opaque given.')
354
355         if 'nonce' not in auth:
356             raise error.LoginFailed('Invalid response, no nonce given.')
357
358         # Now verify the nonce/opaque values for this client
359         if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
360             return DigestedCredentials(username,
361                                        method,
362                                        self.authenticationRealm,
363                                        auth)
364
365
366
367 class CramMD5Credentials:
368     implements(IUsernameHashedPassword)
369
370     challenge = ''
371     response = ''
372
373     def __init__(self, host=None):
374         self.host = host
375
376     def getChallenge(self):
377         if self.challenge:
378             return self.challenge
379         # The data encoded in the first ready response contains an
380         # presumptively arbitrary string of random digits, a timestamp, and
381         # the fully-qualified primary host name of the server.  The syntax of
382         # the unencoded form must correspond to that of an RFC 822 'msg-id'
383         # [RFC822] as described in [POP3].
384         #   -- RFC 2195
385         r = random.randrange(0x7fffffff)
386         t = time.time()
387         self.challenge = '<%d.%d@%s>' % (r, t, self.host)
388         return self.challenge
389
390     def setResponse(self, response):
391         self.username, self.response = response.split(None, 1)
392
393     def moreChallenges(self):
394         return False
395
396     def checkPassword(self, password):
397         verify = hmac.HMAC(password, self.challenge).hexdigest()
398         return verify == self.response
399
400
401 class UsernameHashedPassword:
402     implements(IUsernameHashedPassword)
403
404     def __init__(self, username, hashed):
405         self.username = username
406         self.hashed = hashed
407
408     def checkPassword(self, password):
409         return self.hashed == password
410
411
412 class UsernamePassword:
413     implements(IUsernamePassword)
414
415     def __init__(self, username, password):
416         self.username = username
417         self.password = password
418
419     def checkPassword(self, password):
420         return self.password == password
421
422
423 class Anonymous:
424     implements(IAnonymous)
425
426
427
428 class ISSHPrivateKey(ICredentials):
429     """
430     L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
431     against a user's private key.
432
433     @ivar username: The username associated with these credentials.
434     @type username: C{str}
435
436     @ivar algName: The algorithm name for the blob.
437     @type algName: C{str}
438
439     @ivar blob: The public key blob as sent by the client.
440     @type blob: C{str}
441
442     @ivar sigData: The data the signature was made from.
443     @type sigData: C{str}
444
445     @ivar signature: The signed data.  This is checked to verify that the user
446         owns the private key.
447     @type signature: C{str} or C{NoneType}
448     """
449
450
451
452 class SSHPrivateKey:
453     implements(ISSHPrivateKey)
454     def __init__(self, username, algName, blob, sigData, signature):
455         self.username = username
456         self.algName = algName
457         self.blob = blob
458         self.sigData = sigData
459         self.signature = signature
460
461
462 class IPluggableAuthenticationModules(ICredentials):
463     """I encapsulate the authentication of a user via PAM (Pluggable
464     Authentication Modules.  I use PyPAM (available from
465     http://www.tummy.com/Software/PyPam/index.html).
466
467     @ivar username: The username for the user being logged in.
468
469     @ivar pamConversion: A function that is called with a list of tuples
470     (message, messageType).  See the PAM documentation
471     for the meaning of messageType.  The function
472     returns a Deferred which will fire with a list
473     of (response, 0), one for each message.  The 0 is
474     currently unused, but is required by the PAM library.
475     """
476
477 class PluggableAuthenticationModules:
478     implements(IPluggableAuthenticationModules)
479
480     def __init__(self, username, pamConversion):
481         self.username = username
482         self.pamConversion = pamConversion
483