summaryrefslogtreecommitdiff
path: root/mailman/passwords.py
diff options
context:
space:
mode:
Diffstat (limited to 'mailman/passwords.py')
-rw-r--r--mailman/passwords.py253
1 files changed, 0 insertions, 253 deletions
diff --git a/mailman/passwords.py b/mailman/passwords.py
deleted file mode 100644
index 0c8284c1c..000000000
--- a/mailman/passwords.py
+++ /dev/null
@@ -1,253 +0,0 @@
-# Copyright (C) 2007-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Password hashing and verification schemes.
-
-Represents passwords using RFC 2307 syntax (as best we can tell).
-"""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Schemes',
- 'make_secret',
- 'check_response',
- ]
-
-
-import os
-import re
-import hmac
-import hashlib
-
-from array import array
-from base64 import urlsafe_b64decode as decode
-from base64 import urlsafe_b64encode as encode
-from munepy import Enum
-
-from mailman.core import errors
-
-SALT_LENGTH = 20 # bytes
-ITERATIONS = 2000
-
-
-
-class PasswordScheme:
- TAG = b''
-
- @staticmethod
- def make_secret(password):
- """Return the hashed password"""
- raise NotImplementedError
-
- @staticmethod
- def check_response(challenge, response):
- """Return True if response matches challenge.
-
- It is expected that the scheme specifier prefix is already stripped
- from the response string.
- """
- raise NotImplementedError
-
-
-
-class NoPasswordScheme(PasswordScheme):
- TAG = b'NONE'
-
- @staticmethod
- def make_secret(password):
- return b''
-
- @staticmethod
- def check_response(challenge, response):
- return False
-
-
-
-class ClearTextPasswordScheme(PasswordScheme):
- TAG = b'CLEARTEXT'
-
- @staticmethod
- def make_secret(password):
- return password
-
- @staticmethod
- def check_response(challenge, response):
- return challenge == response
-
-
-
-class SHAPasswordScheme(PasswordScheme):
- TAG = b'SHA'
-
- @staticmethod
- def make_secret(password):
- h = hashlib.sha1(password)
- return encode(h.digest())
-
- @staticmethod
- def check_response(challenge, response):
- h = hashlib.sha1(response)
- return challenge == encode(h.digest())
-
-
-
-class SSHAPasswordScheme(PasswordScheme):
- TAG = b'SSHA'
-
- @staticmethod
- def make_secret(password):
- salt = os.urandom(SALT_LENGTH)
- h = hashlib.sha1(password)
- h.update(salt)
- return encode(h.digest() + salt)
-
- @staticmethod
- def check_response(challenge, response):
- # Get the salt from the challenge
- challenge_bytes = decode(challenge)
- digest = challenge_bytes[:20]
- salt = challenge_bytes[20:]
- h = hashlib.sha1(response)
- h.update(salt)
- return digest == h.digest()
-
-
-
-# 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 = b'PBKDF2 SHA {0}'.format(ITERATIONS)
-
- @staticmethod
- def _pbkdf2(password, salt, iterations):
- """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output
- case. Output of 20 bytes means always exactly one block to handle,
- and a constant block counter appended to the salt in the initial hmac
- update.
- """
- h = hmac.new(password, None, hashlib.sha1)
- prf = h.copy()
- prf.update(salt + b'\x00\x00\x00\x01')
- T = U = array(b'l', prf.digest())
- while iterations:
- prf = h.copy()
- prf.update(U.tostring())
- U = array(b'l', prf.digest())
- T = array(b'l', (t ^ u for t, u in zip(T, U)))
- iterations -= 1
- return T.tostring()
-
- @staticmethod
- def make_secret(password):
- """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output
- case. Output of 20 bytes means always exactly one block to handle,
- and a constant block counter appended to the salt in the initial hmac
- update.
- """
- salt = os.urandom(SALT_LENGTH)
- digest = PBKDF2PasswordScheme._pbkdf2(password, salt, ITERATIONS)
- derived_key = encode(digest + salt)
- return derived_key
-
- @staticmethod
- def check_response(challenge, response, prf, iterations):
- # Decode the challenge to get the number of iterations and salt
- # XXX we don't support anything but sha prf
- if prf.lower() <> b'sha':
- return False
- try:
- iterations = int(iterations)
- except (ValueError, TypeError):
- return False
- challenge_bytes = decode(challenge)
- digest = challenge_bytes[:20]
- salt = challenge_bytes[20:]
- key = PBKDF2PasswordScheme._pbkdf2(response, salt, iterations)
- return digest == key
-
-
-
-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,
- }
-
-
-# Some scheme tags have arguments, but the key for this dictionary should just
-# be the lowercased scheme name.
-_SCHEMES_BY_TAG = dict((_SCHEMES_BY_ENUM[e].TAG.split(' ')[0].lower(), e)
- for e in _SCHEMES_BY_ENUM)
-
-_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)
- if not scheme_class:
- raise errors.BadPasswordSchemeError(scheme)
- secret = scheme_class.make_secret(password)
- return b'{{{0}}}{1}'.format(scheme_class.TAG, secret)
-
-
-def check_response(challenge, response):
- mo = re.match(r'{(?P<scheme>[^}]+?)}(?P<rest>.*)',
- challenge, re.IGNORECASE)
- if not mo:
- return False
- # 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_enum = _SCHEMES_BY_TAG.get(scheme, _DEFAULT_SCHEME)
- scheme_class = _SCHEMES_BY_ENUM[scheme_enum]
- if isinstance(rest_group, unicode):
- rest_group = rest_group.encode('utf-8')
- return scheme_class.check_response(rest_group, response, *scheme_parts[1:])
-
-
-def lookup_scheme(scheme_name):
- return _SCHEMES_BY_TAG.get(scheme_name.lower())