Open Media Library Platform
This commit is contained in:
commit
411ad5b16f
5849 changed files with 1778641 additions and 0 deletions
493
Darwin/lib/python2.7/site-packages/twisted/cred/credentials.py
Normal file
493
Darwin/lib/python2.7/site-packages/twisted/cred/credentials.py
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
# -*- test-case-name: twisted.test.test_newcred-*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module defines L{ICredentials}, an interface for objects that represent
|
||||
authentication credentials to provide, and also includes a number of useful
|
||||
implementations of that interface.
|
||||
"""
|
||||
|
||||
from zope.interface import implements, Interface
|
||||
|
||||
import hmac, time, random, re
|
||||
from hashlib import md5
|
||||
|
||||
from twisted.python.randbytes import secureRandom
|
||||
from twisted.cred._digest import calcResponse, calcHA1, calcHA2
|
||||
from twisted.cred import error
|
||||
|
||||
class ICredentials(Interface):
|
||||
"""
|
||||
I check credentials.
|
||||
|
||||
Implementors _must_ specify which sub-interfaces of ICredentials
|
||||
to which it conforms, using zope.interface.implements().
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class IUsernameDigestHash(ICredentials):
|
||||
"""
|
||||
This credential is used when a CredentialChecker has access to the hash
|
||||
of the username:realm:password as in an Apache .htdigest file.
|
||||
"""
|
||||
def checkHash(digestHash):
|
||||
"""
|
||||
@param digestHash: The hashed username:realm:password to check against.
|
||||
|
||||
@return: C{True} if the credentials represented by this object match
|
||||
the given hash, C{False} if they do not, or a L{Deferred} which
|
||||
will be called back with one of these values.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class IUsernameHashedPassword(ICredentials):
|
||||
"""
|
||||
I encapsulate a username and a hashed password.
|
||||
|
||||
This credential is used when a hashed password is received from the
|
||||
party requesting authentication. CredentialCheckers which check this
|
||||
kind of credential must store the passwords in plaintext (or as
|
||||
password-equivalent hashes) form so that they can be hashed in a manner
|
||||
appropriate for the particular credentials class.
|
||||
|
||||
@type username: C{str}
|
||||
@ivar username: The username associated with these credentials.
|
||||
"""
|
||||
|
||||
def checkPassword(password):
|
||||
"""
|
||||
Validate these credentials against the correct password.
|
||||
|
||||
@type password: C{str}
|
||||
@param password: The correct, plaintext password against which to
|
||||
check.
|
||||
|
||||
@rtype: C{bool} or L{Deferred}
|
||||
@return: C{True} if the credentials represented by this object match the
|
||||
given password, C{False} if they do not, or a L{Deferred} which will
|
||||
be called back with one of these values.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class IUsernamePassword(ICredentials):
|
||||
"""
|
||||
I encapsulate a username and a plaintext password.
|
||||
|
||||
This encapsulates the case where the password received over the network
|
||||
has been hashed with the identity function (That is, not at all). The
|
||||
CredentialsChecker may store the password in whatever format it desires,
|
||||
it need only transform the stored password in a similar way before
|
||||
performing the comparison.
|
||||
|
||||
@type username: C{str}
|
||||
@ivar username: The username associated with these credentials.
|
||||
|
||||
@type password: C{str}
|
||||
@ivar password: The password associated with these credentials.
|
||||
"""
|
||||
|
||||
def checkPassword(password):
|
||||
"""
|
||||
Validate these credentials against the correct password.
|
||||
|
||||
@type password: C{str}
|
||||
@param password: The correct, plaintext password against which to
|
||||
check.
|
||||
|
||||
@rtype: C{bool} or L{Deferred}
|
||||
@return: C{True} if the credentials represented by this object match the
|
||||
given password, C{False} if they do not, or a L{Deferred} which will
|
||||
be called back with one of these values.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class IAnonymous(ICredentials):
|
||||
"""
|
||||
I am an explicitly anonymous request for access.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class DigestedCredentials(object):
|
||||
"""
|
||||
Yet Another Simple HTTP Digest authentication scheme.
|
||||
"""
|
||||
implements(IUsernameHashedPassword, IUsernameDigestHash)
|
||||
|
||||
def __init__(self, username, method, realm, fields):
|
||||
self.username = username
|
||||
self.method = method
|
||||
self.realm = realm
|
||||
self.fields = fields
|
||||
|
||||
|
||||
def checkPassword(self, password):
|
||||
"""
|
||||
Verify that the credentials represented by this object agree with the
|
||||
given plaintext C{password} by hashing C{password} in the same way the
|
||||
response hash represented by this object was generated and comparing
|
||||
the results.
|
||||
"""
|
||||
response = self.fields.get('response')
|
||||
uri = self.fields.get('uri')
|
||||
nonce = self.fields.get('nonce')
|
||||
cnonce = self.fields.get('cnonce')
|
||||
nc = self.fields.get('nc')
|
||||
algo = self.fields.get('algorithm', 'md5').lower()
|
||||
qop = self.fields.get('qop', 'auth')
|
||||
|
||||
expected = calcResponse(
|
||||
calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
|
||||
calcHA2(algo, self.method, uri, qop, None),
|
||||
algo, nonce, nc, cnonce, qop)
|
||||
|
||||
return expected == response
|
||||
|
||||
|
||||
def checkHash(self, digestHash):
|
||||
"""
|
||||
Verify that the credentials represented by this object agree with the
|
||||
credentials represented by the I{H(A1)} given in C{digestHash}.
|
||||
|
||||
@param digestHash: A precomputed H(A1) value based on the username,
|
||||
realm, and password associate with this credentials object.
|
||||
"""
|
||||
response = self.fields.get('response')
|
||||
uri = self.fields.get('uri')
|
||||
nonce = self.fields.get('nonce')
|
||||
cnonce = self.fields.get('cnonce')
|
||||
nc = self.fields.get('nc')
|
||||
algo = self.fields.get('algorithm', 'md5').lower()
|
||||
qop = self.fields.get('qop', 'auth')
|
||||
|
||||
expected = calcResponse(
|
||||
calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
|
||||
calcHA2(algo, self.method, uri, qop, None),
|
||||
algo, nonce, nc, cnonce, qop)
|
||||
|
||||
return expected == response
|
||||
|
||||
|
||||
|
||||
class DigestCredentialFactory(object):
|
||||
"""
|
||||
Support for RFC2617 HTTP Digest Authentication
|
||||
|
||||
@cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
|
||||
opaque should be valid.
|
||||
|
||||
@type privateKey: C{str}
|
||||
@ivar privateKey: A random string used for generating the secure opaque.
|
||||
|
||||
@type algorithm: C{str}
|
||||
@param algorithm: Case insensitive string specifying the hash algorithm to
|
||||
use. Must be either C{'md5'} or C{'sha'}. C{'md5-sess'} is B{not}
|
||||
supported.
|
||||
|
||||
@type authenticationRealm: C{str}
|
||||
@param authenticationRealm: case sensitive string that specifies the realm
|
||||
portion of the challenge
|
||||
"""
|
||||
|
||||
_parseparts = re.compile(
|
||||
b'([^= ]+)' # The key
|
||||
b'=' # Conventional key/value separator (literal)
|
||||
b'(?:' # Group together a couple options
|
||||
b'"([^"]*)"' # A quoted string of length 0 or more
|
||||
b'|' # The other option in the group is coming
|
||||
b'([^,]+)' # An unquoted string of length 1 or more, up to a comma
|
||||
b')' # That non-matching group ends
|
||||
b',?') # There might be a comma at the end (none on last pair)
|
||||
|
||||
CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
|
||||
|
||||
scheme = "digest"
|
||||
|
||||
def __init__(self, algorithm, authenticationRealm):
|
||||
self.algorithm = algorithm
|
||||
self.authenticationRealm = authenticationRealm
|
||||
self.privateKey = secureRandom(12)
|
||||
|
||||
|
||||
def getChallenge(self, address):
|
||||
"""
|
||||
Generate the challenge for use in the WWW-Authenticate header.
|
||||
|
||||
@param address: The client address to which this challenge is being
|
||||
sent.
|
||||
|
||||
@return: The C{dict} that can be used to generate a WWW-Authenticate
|
||||
header.
|
||||
"""
|
||||
c = self._generateNonce()
|
||||
o = self._generateOpaque(c, address)
|
||||
|
||||
return {'nonce': c,
|
||||
'opaque': o,
|
||||
'qop': 'auth',
|
||||
'algorithm': self.algorithm,
|
||||
'realm': self.authenticationRealm}
|
||||
|
||||
|
||||
def _generateNonce(self):
|
||||
"""
|
||||
Create a random value suitable for use as the nonce parameter of a
|
||||
WWW-Authenticate challenge.
|
||||
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return secureRandom(12).encode('hex')
|
||||
|
||||
|
||||
def _getTime(self):
|
||||
"""
|
||||
Parameterize the time based seed used in C{_generateOpaque}
|
||||
so we can deterministically unittest it's behavior.
|
||||
"""
|
||||
return time.time()
|
||||
|
||||
|
||||
def _generateOpaque(self, nonce, clientip):
|
||||
"""
|
||||
Generate an opaque to be returned to the client. This is a unique
|
||||
string that can be returned to us and verified.
|
||||
"""
|
||||
# Now, what we do is encode the nonce, client ip and a timestamp in the
|
||||
# opaque value with a suitable digest.
|
||||
now = str(int(self._getTime()))
|
||||
if clientip is None:
|
||||
clientip = ''
|
||||
key = "%s,%s,%s" % (nonce, clientip, now)
|
||||
digest = md5(key + self.privateKey).hexdigest()
|
||||
ekey = key.encode('base64')
|
||||
return "%s-%s" % (digest, ekey.replace('\n', ''))
|
||||
|
||||
|
||||
def _verifyOpaque(self, opaque, nonce, clientip):
|
||||
"""
|
||||
Given the opaque and nonce from the request, as well as the client IP
|
||||
that made the request, verify that the opaque was generated by us.
|
||||
And that it's not too old.
|
||||
|
||||
@param opaque: The opaque value from the Digest response
|
||||
@param nonce: The nonce value from the Digest response
|
||||
@param clientip: The remote IP address of the client making the request
|
||||
or C{None} if the request was submitted over a channel where this
|
||||
does not make sense.
|
||||
|
||||
@return: C{True} if the opaque was successfully verified.
|
||||
|
||||
@raise error.LoginFailed: if C{opaque} could not be parsed or
|
||||
contained the wrong values.
|
||||
"""
|
||||
# First split the digest from the key
|
||||
opaqueParts = opaque.split('-')
|
||||
if len(opaqueParts) != 2:
|
||||
raise error.LoginFailed('Invalid response, invalid opaque value')
|
||||
|
||||
if clientip is None:
|
||||
clientip = ''
|
||||
|
||||
# Verify the key
|
||||
key = opaqueParts[1].decode('base64')
|
||||
keyParts = key.split(',')
|
||||
|
||||
if len(keyParts) != 3:
|
||||
raise error.LoginFailed('Invalid response, invalid opaque value')
|
||||
|
||||
if keyParts[0] != nonce:
|
||||
raise error.LoginFailed(
|
||||
'Invalid response, incompatible opaque/nonce values')
|
||||
|
||||
if keyParts[1] != clientip:
|
||||
raise error.LoginFailed(
|
||||
'Invalid response, incompatible opaque/client values')
|
||||
|
||||
try:
|
||||
when = int(keyParts[2])
|
||||
except ValueError:
|
||||
raise error.LoginFailed(
|
||||
'Invalid response, invalid opaque/time values')
|
||||
|
||||
if (int(self._getTime()) - when >
|
||||
DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
|
||||
|
||||
raise error.LoginFailed(
|
||||
'Invalid response, incompatible opaque/nonce too old')
|
||||
|
||||
# Verify the digest
|
||||
digest = md5(key + self.privateKey).hexdigest()
|
||||
if digest != opaqueParts[0]:
|
||||
raise error.LoginFailed('Invalid response, invalid opaque value')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def decode(self, response, method, host):
|
||||
"""
|
||||
Decode the given response and attempt to generate a
|
||||
L{DigestedCredentials} from it.
|
||||
|
||||
@type response: C{str}
|
||||
@param response: A string of comma seperated key=value pairs
|
||||
|
||||
@type method: C{str}
|
||||
@param method: The action requested to which this response is addressed
|
||||
(GET, POST, INVITE, OPTIONS, etc).
|
||||
|
||||
@type host: C{str}
|
||||
@param host: The address the request was sent from.
|
||||
|
||||
@raise error.LoginFailed: If the response does not contain a username,
|
||||
a nonce, an opaque, or if the opaque is invalid.
|
||||
|
||||
@return: L{DigestedCredentials}
|
||||
"""
|
||||
response = ' '.join(response.splitlines())
|
||||
parts = self._parseparts.findall(response)
|
||||
auth = {}
|
||||
for (key, bare, quoted) in parts:
|
||||
value = (quoted or bare).strip()
|
||||
auth[key.strip()] = value
|
||||
|
||||
username = auth.get('username')
|
||||
if not username:
|
||||
raise error.LoginFailed('Invalid response, no username given.')
|
||||
|
||||
if 'opaque' not in auth:
|
||||
raise error.LoginFailed('Invalid response, no opaque given.')
|
||||
|
||||
if 'nonce' not in auth:
|
||||
raise error.LoginFailed('Invalid response, no nonce given.')
|
||||
|
||||
# Now verify the nonce/opaque values for this client
|
||||
if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
|
||||
return DigestedCredentials(username,
|
||||
method,
|
||||
self.authenticationRealm,
|
||||
auth)
|
||||
|
||||
|
||||
|
||||
class CramMD5Credentials:
|
||||
implements(IUsernameHashedPassword)
|
||||
|
||||
challenge = ''
|
||||
response = ''
|
||||
|
||||
def __init__(self, host=None):
|
||||
self.host = host
|
||||
|
||||
def getChallenge(self):
|
||||
if self.challenge:
|
||||
return self.challenge
|
||||
# The data encoded in the first ready response contains an
|
||||
# presumptively arbitrary string of random digits, a timestamp, and
|
||||
# the fully-qualified primary host name of the server. The syntax of
|
||||
# the unencoded form must correspond to that of an RFC 822 'msg-id'
|
||||
# [RFC822] as described in [POP3].
|
||||
# -- RFC 2195
|
||||
r = random.randrange(0x7fffffff)
|
||||
t = time.time()
|
||||
self.challenge = '<%d.%d@%s>' % (r, t, self.host)
|
||||
return self.challenge
|
||||
|
||||
def setResponse(self, response):
|
||||
self.username, self.response = response.split(None, 1)
|
||||
|
||||
def moreChallenges(self):
|
||||
return False
|
||||
|
||||
def checkPassword(self, password):
|
||||
verify = hmac.HMAC(password, self.challenge).hexdigest()
|
||||
return verify == self.response
|
||||
|
||||
|
||||
class UsernameHashedPassword:
|
||||
implements(IUsernameHashedPassword)
|
||||
|
||||
def __init__(self, username, hashed):
|
||||
self.username = username
|
||||
self.hashed = hashed
|
||||
|
||||
def checkPassword(self, password):
|
||||
return self.hashed == password
|
||||
|
||||
|
||||
class UsernamePassword:
|
||||
implements(IUsernamePassword)
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def checkPassword(self, password):
|
||||
return self.password == password
|
||||
|
||||
|
||||
class Anonymous:
|
||||
implements(IAnonymous)
|
||||
|
||||
|
||||
|
||||
class ISSHPrivateKey(ICredentials):
|
||||
"""
|
||||
L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
|
||||
against a user's private key.
|
||||
|
||||
@ivar username: The username associated with these credentials.
|
||||
@type username: C{str}
|
||||
|
||||
@ivar algName: The algorithm name for the blob.
|
||||
@type algName: C{str}
|
||||
|
||||
@ivar blob: The public key blob as sent by the client.
|
||||
@type blob: C{str}
|
||||
|
||||
@ivar sigData: The data the signature was made from.
|
||||
@type sigData: C{str}
|
||||
|
||||
@ivar signature: The signed data. This is checked to verify that the user
|
||||
owns the private key.
|
||||
@type signature: C{str} or C{NoneType}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class SSHPrivateKey:
|
||||
implements(ISSHPrivateKey)
|
||||
def __init__(self, username, algName, blob, sigData, signature):
|
||||
self.username = username
|
||||
self.algName = algName
|
||||
self.blob = blob
|
||||
self.sigData = sigData
|
||||
self.signature = signature
|
||||
|
||||
|
||||
class IPluggableAuthenticationModules(ICredentials):
|
||||
"""I encapsulate the authentication of a user via PAM (Pluggable
|
||||
Authentication Modules. I use PyPAM (available from
|
||||
http://www.tummy.com/Software/PyPam/index.html).
|
||||
|
||||
@ivar username: The username for the user being logged in.
|
||||
|
||||
@ivar pamConversion: A function that is called with a list of tuples
|
||||
(message, messageType). See the PAM documentation
|
||||
for the meaning of messageType. The function
|
||||
returns a Deferred which will fire with a list
|
||||
of (response, 0), one for each message. The 0 is
|
||||
currently unused, but is required by the PAM library.
|
||||
"""
|
||||
|
||||
class PluggableAuthenticationModules:
|
||||
implements(IPluggableAuthenticationModules)
|
||||
|
||||
def __init__(self, username, pamConversion):
|
||||
self.username = username
|
||||
self.pamConversion = pamConversion
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue