summaryrefslogtreecommitdiff
path: root/Mailman/passwords.py
diff options
context:
space:
mode:
authorbwarsaw2007-03-02 00:07:43 +0000
committerbwarsaw2007-03-02 00:07:43 +0000
commit173997db20a1fad4ec6a78e0c7a88e147869cd7d (patch)
tree30df8131cd8131be4299e0dd23b601b83f6c3600 /Mailman/passwords.py
parentc08b190d0b31d09ef25a0e213b719ff337ac533c (diff)
downloadmailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.tar.gz
mailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.tar.zst
mailman-173997db20a1fad4ec6a78e0c7a88e147869cd7d.zip
Diffstat (limited to 'Mailman/passwords.py')
-rw-r--r--Mailman/passwords.py101
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:])