diff options
| author | bwarsaw | 2007-03-02 00:07:43 +0000 |
|---|---|---|
| committer | bwarsaw | 2007-03-02 00:07:43 +0000 |
| commit | 173997db20a1fad4ec6a78e0c7a88e147869cd7d (patch) | |
| tree | 30df8131cd8131be4299e0dd23b601b83f6c3600 /Mailman/passwords.py | |
| parent | c08b190d0b31d09ef25a0e213b719ff337ac533c (diff) | |
| download | mailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.tar.gz mailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.tar.zst mailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.zip | |
Diffstat (limited to 'Mailman/passwords.py')
| -rw-r--r-- | Mailman/passwords.py | 101 |
1 files changed, 79 insertions, 22 deletions
diff --git a/Mailman/passwords.py b/Mailman/passwords.py index 0e02c605c..e019d061e 100644 --- a/Mailman/passwords.py +++ b/Mailman/passwords.py @@ -17,7 +17,7 @@ """Password hashing and verification schemes. -Represents passwords using RFC 2307 syntax. +Represents passwords using RFC 2307 syntax (as best we can tell). """ import os @@ -29,15 +29,25 @@ from array import array from base64 import urlsafe_b64decode as decode from base64 import urlsafe_b64encode as encode +from Mailman.enum import Enum + SALT_LENGTH = 20 # bytes ITERATIONS = 2000 +__all__ = [ + 'Schemes', + 'make_secret', + 'check_response', + ] + class PasswordScheme(object): + TAG = '' + @staticmethod def make_secret(password): - """Return the hashed password""" + """Return the hashed password""" raise NotImplementedError @staticmethod @@ -52,9 +62,11 @@ class PasswordScheme(object): class NoPasswordScheme(PasswordScheme): + TAG = 'NONE' + @staticmethod def make_secret(password): - return '{NONE}' + return '' @staticmethod def check_response(challenge, response): @@ -63,9 +75,11 @@ class NoPasswordScheme(PasswordScheme): class ClearTextPasswordScheme(PasswordScheme): + TAG = 'CLEARTEXT' + @staticmethod def make_secret(password): - return '{CLEARTEXT}' + password + return password @staticmethod def check_response(challenge, response): @@ -74,10 +88,12 @@ class ClearTextPasswordScheme(PasswordScheme): class SHAPasswordScheme(PasswordScheme): + TAG = 'SHA' + @staticmethod def make_secret(password): h = sha.new(password) - return '{SHA}' + encode(h.digest()) + return encode(h.digest()) @staticmethod def check_response(challenge, response): @@ -87,17 +103,19 @@ class SHAPasswordScheme(PasswordScheme): class SSHAPasswordScheme(PasswordScheme): + TAG = 'SSHA' + @staticmethod def make_secret(password): salt = os.urandom(SALT_LENGTH) h = sha.new(password) h.update(salt) - return '{SSHA}' + encode(h.digest() + salt) + return encode(h.digest() + salt) @staticmethod def check_response(challenge, response): # Get the salt from the challenge - challenge_bytes = decode(str(challenge)) + challenge_bytes = decode(challenge) digest = challenge_bytes[:20] salt = challenge_bytes[20:] h = sha.new(response) @@ -106,8 +124,13 @@ class SSHAPasswordScheme(PasswordScheme): -# Given by Bob Fleck +# Basic algorithm given by Bob Fleck class PBKDF2PasswordScheme(PasswordScheme): + # This is a bit nasty if we wanted a different prf or iterations. OTOH, + # we really have no clue what the standard LDAP-ish specification for + # those options is. + TAG = 'PBKDF2 SHA %d' % ITERATIONS + @staticmethod def _pbkdf2(password, salt, iterations): """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output @@ -137,7 +160,7 @@ class PBKDF2PasswordScheme(PasswordScheme): salt = os.urandom(SALT_LENGTH) digest = PBKDF2PasswordScheme._pbkdf2(password, salt, ITERATIONS) derived_key = encode(digest + salt) - return '{PBKDF2 SHA %d}' % ITERATIONS + derived_key + return derived_key @staticmethod def check_response(challenge, response, prf, iterations): @@ -157,18 +180,47 @@ class PBKDF2PasswordScheme(PasswordScheme): -SCHEMES = { - 'none' : NoPasswordScheme, - 'cleartext' : ClearTextPasswordScheme, - 'sha' : SHAPasswordScheme, - 'ssha' : SSHAPasswordScheme, - 'pbkdf2' : PBKDF2PasswordScheme, +class Schemes(Enum): + # no_scheme is deliberately ugly because no one should be using it. Yes, + # this makes cleartext inconsistent, but that's a common enough + # terminology to justify the missing underscore. + no_scheme = 1 + cleartext = 2 + sha = 3 + ssha = 4 + pbkdf2 = 5 + + +_SCHEMES_BY_ENUM = { + Schemes.no_scheme : NoPasswordScheme, + Schemes.cleartext : ClearTextPasswordScheme, + Schemes.sha : SHAPasswordScheme, + Schemes.ssha : SSHAPasswordScheme, + Schemes.pbkdf2 : PBKDF2PasswordScheme, } -def make_secret(password, scheme): - scheme_class = SCHEMES.get(scheme.lower(), NoPasswordScheme) - return scheme_class.make_secret(password) +# Some scheme tags have arguments, but the key for this dictionary should just +# be the lowercased scheme name. +_SCHEMES_BY_TAG = dict((c.TAG.split(' ')[0].lower(), c) + for c in _SCHEMES_BY_ENUM.values()) + +_DEFAULT_SCHEME = NoPasswordScheme + + + +def make_secret(password, scheme=None): + # The hash algorithms operate on bytes not strings. The password argument + # as provided here by the client will be a string (in Python 2 either + # unicode or 8-bit, in Python 3 always unicode). We need to encode this + # string into a byte array, and the way to spell that in Python 2 is to + # encode the string to utf-8. The returned secret is a string, so it must + # be a unicode. + if isinstance(password, unicode): + password = password.encode('utf-8') + scheme_class = _SCHEMES_BY_ENUM.get(scheme, _DEFAULT_SCHEME) + secret = scheme_class.make_secret(password) + return '{%s}%s' % (scheme_class.TAG, secret) def check_response(challenge, response): @@ -176,7 +228,12 @@ def check_response(challenge, response): challenge, re.IGNORECASE) if not mo: return False - scheme, rest = mo.group('scheme', 'rest') - scheme_parts = scheme.split() - scheme_class = SCHEMES.get(scheme_parts[0].lower(), NoPasswordScheme) - return scheme_class.check_response(rest, response, *scheme_parts[1:]) + # See above for why we convert here. However because we should have + # generated the challenge, we assume that it is already a byte string. + if isinstance(response, unicode): + response = response.encode('utf-8') + scheme_group, rest_group = mo.group('scheme', 'rest') + scheme_parts = scheme_group.split() + scheme = scheme_parts[0].lower() + scheme_class = _SCHEMES_BY_TAG.get(scheme, _DEFAULT_SCHEME) + return scheme_class.check_response(rest_group, response, *scheme_parts[1:]) |
