From 51140e885c9e1dc074e1fb3f288f50a8e9add884 Mon Sep 17 00:00:00 2001
From: Barry Warsaw
Date: Fri, 25 Feb 2011 15:29:19 -0500
Subject: Move passwords module to the utilities subpackage.
---
src/mailman/Utils.py | 4 -
src/mailman/passwords.py | 317 --------------------------
src/mailman/tests/test_membership.py | 2 +-
src/mailman/tests/test_passwords.py | 150 ------------
src/mailman/utilities/passwords.py | 317 ++++++++++++++++++++++++++
src/mailman/utilities/tests/test_passwords.py | 150 ++++++++++++
6 files changed, 468 insertions(+), 472 deletions(-)
delete mode 100644 src/mailman/passwords.py
delete mode 100644 src/mailman/tests/test_passwords.py
create mode 100644 src/mailman/utilities/passwords.py
create mode 100644 src/mailman/utilities/tests/test_passwords.py
diff --git a/src/mailman/Utils.py b/src/mailman/Utils.py
index 384d25702..49b7d082e 100644
--- a/src/mailman/Utils.py
+++ b/src/mailman/Utils.py
@@ -33,20 +33,16 @@ import os
import re
import cgi
import errno
-import base64
-import random
import logging
# pylint: disable-msg=E0611,W0403
from email.errors import HeaderParseError
from email.header import decode_header, make_header
-from lazr.config import as_boolean
from string import ascii_letters, digits, whitespace
from zope.component import getUtility
import mailman.templates
-from mailman import passwords
from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.languages import ILanguageManager
diff --git a/src/mailman/passwords.py b/src/mailman/passwords.py
deleted file mode 100644
index c14584748..000000000
--- a/src/mailman/passwords.py
+++ /dev/null
@@ -1,317 +0,0 @@
-# Copyright (C) 2007-2011 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 .
-
-"""Password hashing and verification schemes.
-
-Represents passwords using RFC 2307 syntax (as best we can tell).
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Schemes',
- 'check_response',
- 'make_secret',
- ]
-
-
-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 flufl.enum import Enum
-
-from mailman.core import errors
-
-SALT_LENGTH = 20 # bytes
-ITERATIONS = 2000
-
-
-
-# pylint: disable-msg=W0232
-class PasswordScheme:
- """Password scheme base class."""
- TAG = b''
-
- @staticmethod
- def make_secret(password):
- """Return the hashed password.
-
- :param password: The clear text password.
- :type password: string
- :return: The encrypted password.
- :rtype: string
- """
- raise NotImplementedError
-
- @staticmethod
- def check_response(challenge, response):
- """Check a response against a challenge.
-
- It is expected that the scheme specifier prefix is already stripped
- from the response string.
-
- :param challenge: The challenge.
- :type challenge: string
- :param response: The response.
- :type response: string
- :return: True if the response matches the challenge.
- :rtype: bool
- """
- raise NotImplementedError
-
-
-
-class NoPasswordScheme(PasswordScheme):
- """A password scheme without passwords."""
-
- TAG = b'NONE'
-
- @staticmethod
- def make_secret(password):
- """See `PasswordScheme`."""
- return b''
-
- # pylint: disable-msg=W0613
- @staticmethod
- def check_response(challenge, response):
- """See `PasswordScheme`."""
- return False
-
-
-
-class ClearTextPasswordScheme(PasswordScheme):
- """A password scheme that stores clear text passwords."""
-
- TAG = b'CLEARTEXT'
-
- @staticmethod
- def make_secret(password):
- """See `PasswordScheme`."""
- return password
-
- @staticmethod
- def check_response(challenge, response):
- """See `PasswordScheme`."""
- return challenge == response
-
-
-
-class SHAPasswordScheme(PasswordScheme):
- """A password scheme that encodes the password using SHA1."""
-
- TAG = b'SHA'
-
- @staticmethod
- def make_secret(password):
- """See `PasswordScheme`."""
- h = hashlib.sha1(password)
- return encode(h.digest())
-
- @staticmethod
- def check_response(challenge, response):
- """See `PasswordScheme`."""
- h = hashlib.sha1(response)
- return challenge == encode(h.digest())
-
-
-
-class SSHAPasswordScheme(PasswordScheme):
- """A password scheme that encodes the password using salted SHA1."""
-
- TAG = b'SSHA'
-
- @staticmethod
- def make_secret(password):
- """See `PasswordScheme`."""
- salt = os.urandom(SALT_LENGTH)
- h = hashlib.sha1(password)
- h.update(salt)
- return encode(h.digest() + salt)
-
- @staticmethod
- def check_response(challenge, response):
- """See `PasswordScheme`."""
- # 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):
- """RFC 2989 password encoding scheme."""
-
- # 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'i', prf.digest())
- while iterations:
- prf = h.copy()
- prf.update(U.tostring())
- U = array(b'i', prf.digest())
- T = array(b'i', (t ^ u for t, u in zip(T, U)))
- iterations -= 1
- return T.tostring()
-
- @staticmethod
- def make_secret(password):
- """See `PasswordScheme`.
-
- 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
-
- # pylint: disable-msg=W0221
- @staticmethod
- def check_response(challenge, response, prf, iterations):
- """See `PasswordScheme`."""
- # Decode the challenge to get the number of iterations and salt. 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):
- """List of password schemes."""
- # 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):
- """Encrypt a password.
-
- :param password: The clear text password.
- :type password: string
- :param scheme: The password scheme name.
- :type scheme: string
- :return: The encrypted password.
- :rtype: string
- """
- # 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):
- """Check a response against a challenge.
-
- :param challenge: The challenge.
- :type challenge: string
- :param response: The response.
- :type response: string
- :return: True if the response matches the challenge.
- :rtype: bool
- """
- mo = re.match(r'{(?P[^}]+?)}(?P.*)',
- 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):
- """Look up a password scheme.
-
- :param scheme_name: The password scheme name.
- :type scheme_name: string
- :return: Password scheme class.
- :rtype: `PasswordScheme`
- """
- return _SCHEMES_BY_TAG.get(scheme_name.lower())
diff --git a/src/mailman/tests/test_membership.py b/src/mailman/tests/test_membership.py
index c3e40cfcc..8425cf65f 100644
--- a/src/mailman/tests/test_membership.py
+++ b/src/mailman/tests/test_membership.py
@@ -28,11 +28,11 @@ __all__ = [
import time
import unittest
-from mailman import passwords
from mailman.app.lifecycle import create_list, remove_list
from mailman.config import config
from mailman.interfaces.member import NotAMemberError
from mailman.testing.layers import ConfigLayer
+from mailman.utilities import passwords
diff --git a/src/mailman/tests/test_passwords.py b/src/mailman/tests/test_passwords.py
deleted file mode 100644
index 49f55e82f..000000000
--- a/src/mailman/tests/test_passwords.py
+++ /dev/null
@@ -1,150 +0,0 @@
-# Copyright (C) 2007-2011 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 .
-
-"""Unit tests for the passwords module."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'test_suite',
- ]
-
-
-import unittest
-
-from mailman import passwords
-from mailman.core import errors
-
-
-
-class TestPasswordsBase(unittest.TestCase):
- scheme = None
-
- def setUp(self):
- # passwords; 8-bit or unicode strings; ascii or binary
- self.pw8a = b'abc'
- self.pw8b = b'abc\xc3\xbf' # 'abc\xff'
- self.pwub = b'abc\xff'
- # bad password; 8-bit or unicode; ascii or binary
- self.bad8a = b'xyz'
- self.bad8b = b'xyz\xc3\xbf' # 'xyz\xff'
- self.badub = b'xyz\xff'
-
- def test_passwords(self):
- unless = self.failUnless
- failif = self.failIf
- secret = passwords.make_secret(self.pw8a, self.scheme)
- unless(passwords.check_response(secret, self.pw8a))
- failif(passwords.check_response(secret, self.bad8a))
-
- def test_passwords_with_funky_chars(self):
- unless = self.failUnless
- failif = self.failIf
- secret = passwords.make_secret(self.pw8b, self.scheme)
- unless(passwords.check_response(secret, self.pw8b))
- failif(passwords.check_response(secret, self.bad8b))
-
- def test_unicode_passwords_with_funky_chars(self):
- unless = self.failUnless
- failif = self.failIf
- secret = passwords.make_secret(self.pwub, self.scheme)
- unless(passwords.check_response(secret, self.pwub))
- failif(passwords.check_response(secret, self.badub))
-
-
-
-class TestBogusPasswords(TestPasswordsBase):
- scheme = -1
-
- def test_passwords(self):
- self.assertRaises(errors.BadPasswordSchemeError,
- passwords.make_secret, self.pw8a, self.scheme)
-
- def test_passwords_with_funky_chars(self):
- self.assertRaises(errors.BadPasswordSchemeError,
- passwords.make_secret, self.pw8b, self.scheme)
-
- def test_unicode_passwords_with_funky_chars(self):
- self.assertRaises(errors.BadPasswordSchemeError,
- passwords.make_secret, self.pwub, self.scheme)
-
-
-
-class TestNonePasswords(TestPasswordsBase):
- scheme = passwords.Schemes.no_scheme
-
- def test_passwords(self):
- failif = self.failIf
- secret = passwords.make_secret(self.pw8a, self.scheme)
- failif(passwords.check_response(secret, self.pw8a))
- failif(passwords.check_response(secret, self.bad8a))
-
- def test_passwords_with_funky_chars(self):
- failif = self.failIf
- secret = passwords.make_secret(self.pw8b, self.scheme)
- failif(passwords.check_response(secret, self.pw8b))
- failif(passwords.check_response(secret, self.bad8b))
-
- def test_unicode_passwords_with_funky_chars(self):
- failif = self.failIf
- secret = passwords.make_secret(self.pwub, self.scheme)
- failif(passwords.check_response(secret, self.pwub))
- failif(passwords.check_response(secret, self.badub))
-
-
-
-class TestCleartextPasswords(TestPasswordsBase):
- scheme = passwords.Schemes.cleartext
-
-
-class TestSHAPasswords(TestPasswordsBase):
- scheme = passwords.Schemes.sha
-
-
-class TestSSHAPasswords(TestPasswordsBase):
- scheme = passwords.Schemes.ssha
-
-
-class TestPBKDF2Passwords(TestPasswordsBase):
- scheme = passwords.Schemes.pbkdf2
-
-
-
-class TestSchemeLookup(unittest.TestCase):
- def test_scheme_name_lookup(self):
- unless = self.failUnless
- unless(passwords.lookup_scheme('NONE') is passwords.Schemes.no_scheme)
- unless(passwords.lookup_scheme('CLEARTEXT') is
- passwords.Schemes.cleartext)
- unless(passwords.lookup_scheme('SHA') is passwords.Schemes.sha)
- unless(passwords.lookup_scheme('SSHA') is passwords.Schemes.ssha)
- unless(passwords.lookup_scheme('PBKDF2') is passwords.Schemes.pbkdf2)
- unless(passwords.lookup_scheme(' -bogus- ') is None)
-
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(TestBogusPasswords))
- suite.addTest(unittest.makeSuite(TestNonePasswords))
- suite.addTest(unittest.makeSuite(TestCleartextPasswords))
- suite.addTest(unittest.makeSuite(TestSHAPasswords))
- suite.addTest(unittest.makeSuite(TestSSHAPasswords))
- suite.addTest(unittest.makeSuite(TestPBKDF2Passwords))
- suite.addTest(unittest.makeSuite(TestSchemeLookup))
- return suite
diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py
new file mode 100644
index 000000000..c14584748
--- /dev/null
+++ b/src/mailman/utilities/passwords.py
@@ -0,0 +1,317 @@
+# Copyright (C) 2007-2011 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 .
+
+"""Password hashing and verification schemes.
+
+Represents passwords using RFC 2307 syntax (as best we can tell).
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Schemes',
+ 'check_response',
+ 'make_secret',
+ ]
+
+
+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 flufl.enum import Enum
+
+from mailman.core import errors
+
+SALT_LENGTH = 20 # bytes
+ITERATIONS = 2000
+
+
+
+# pylint: disable-msg=W0232
+class PasswordScheme:
+ """Password scheme base class."""
+ TAG = b''
+
+ @staticmethod
+ def make_secret(password):
+ """Return the hashed password.
+
+ :param password: The clear text password.
+ :type password: string
+ :return: The encrypted password.
+ :rtype: string
+ """
+ raise NotImplementedError
+
+ @staticmethod
+ def check_response(challenge, response):
+ """Check a response against a challenge.
+
+ It is expected that the scheme specifier prefix is already stripped
+ from the response string.
+
+ :param challenge: The challenge.
+ :type challenge: string
+ :param response: The response.
+ :type response: string
+ :return: True if the response matches the challenge.
+ :rtype: bool
+ """
+ raise NotImplementedError
+
+
+
+class NoPasswordScheme(PasswordScheme):
+ """A password scheme without passwords."""
+
+ TAG = b'NONE'
+
+ @staticmethod
+ def make_secret(password):
+ """See `PasswordScheme`."""
+ return b''
+
+ # pylint: disable-msg=W0613
+ @staticmethod
+ def check_response(challenge, response):
+ """See `PasswordScheme`."""
+ return False
+
+
+
+class ClearTextPasswordScheme(PasswordScheme):
+ """A password scheme that stores clear text passwords."""
+
+ TAG = b'CLEARTEXT'
+
+ @staticmethod
+ def make_secret(password):
+ """See `PasswordScheme`."""
+ return password
+
+ @staticmethod
+ def check_response(challenge, response):
+ """See `PasswordScheme`."""
+ return challenge == response
+
+
+
+class SHAPasswordScheme(PasswordScheme):
+ """A password scheme that encodes the password using SHA1."""
+
+ TAG = b'SHA'
+
+ @staticmethod
+ def make_secret(password):
+ """See `PasswordScheme`."""
+ h = hashlib.sha1(password)
+ return encode(h.digest())
+
+ @staticmethod
+ def check_response(challenge, response):
+ """See `PasswordScheme`."""
+ h = hashlib.sha1(response)
+ return challenge == encode(h.digest())
+
+
+
+class SSHAPasswordScheme(PasswordScheme):
+ """A password scheme that encodes the password using salted SHA1."""
+
+ TAG = b'SSHA'
+
+ @staticmethod
+ def make_secret(password):
+ """See `PasswordScheme`."""
+ salt = os.urandom(SALT_LENGTH)
+ h = hashlib.sha1(password)
+ h.update(salt)
+ return encode(h.digest() + salt)
+
+ @staticmethod
+ def check_response(challenge, response):
+ """See `PasswordScheme`."""
+ # 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):
+ """RFC 2989 password encoding scheme."""
+
+ # 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'i', prf.digest())
+ while iterations:
+ prf = h.copy()
+ prf.update(U.tostring())
+ U = array(b'i', prf.digest())
+ T = array(b'i', (t ^ u for t, u in zip(T, U)))
+ iterations -= 1
+ return T.tostring()
+
+ @staticmethod
+ def make_secret(password):
+ """See `PasswordScheme`.
+
+ 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
+
+ # pylint: disable-msg=W0221
+ @staticmethod
+ def check_response(challenge, response, prf, iterations):
+ """See `PasswordScheme`."""
+ # Decode the challenge to get the number of iterations and salt. 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):
+ """List of password schemes."""
+ # 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):
+ """Encrypt a password.
+
+ :param password: The clear text password.
+ :type password: string
+ :param scheme: The password scheme name.
+ :type scheme: string
+ :return: The encrypted password.
+ :rtype: string
+ """
+ # 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):
+ """Check a response against a challenge.
+
+ :param challenge: The challenge.
+ :type challenge: string
+ :param response: The response.
+ :type response: string
+ :return: True if the response matches the challenge.
+ :rtype: bool
+ """
+ mo = re.match(r'{(?P[^}]+?)}(?P.*)',
+ 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):
+ """Look up a password scheme.
+
+ :param scheme_name: The password scheme name.
+ :type scheme_name: string
+ :return: Password scheme class.
+ :rtype: `PasswordScheme`
+ """
+ return _SCHEMES_BY_TAG.get(scheme_name.lower())
diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py
new file mode 100644
index 000000000..60c201a5a
--- /dev/null
+++ b/src/mailman/utilities/tests/test_passwords.py
@@ -0,0 +1,150 @@
+# Copyright (C) 2007-2011 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 .
+
+"""Unit tests for the passwords module."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
+import unittest
+
+from mailman.core import errors
+from mailman.utilities import passwords
+
+
+
+class TestPasswordsBase(unittest.TestCase):
+ scheme = None
+
+ def setUp(self):
+ # passwords; 8-bit or unicode strings; ascii or binary
+ self.pw8a = b'abc'
+ self.pw8b = b'abc\xc3\xbf' # 'abc\xff'
+ self.pwub = b'abc\xff'
+ # bad password; 8-bit or unicode; ascii or binary
+ self.bad8a = b'xyz'
+ self.bad8b = b'xyz\xc3\xbf' # 'xyz\xff'
+ self.badub = b'xyz\xff'
+
+ def test_passwords(self):
+ unless = self.failUnless
+ failif = self.failIf
+ secret = passwords.make_secret(self.pw8a, self.scheme)
+ unless(passwords.check_response(secret, self.pw8a))
+ failif(passwords.check_response(secret, self.bad8a))
+
+ def test_passwords_with_funky_chars(self):
+ unless = self.failUnless
+ failif = self.failIf
+ secret = passwords.make_secret(self.pw8b, self.scheme)
+ unless(passwords.check_response(secret, self.pw8b))
+ failif(passwords.check_response(secret, self.bad8b))
+
+ def test_unicode_passwords_with_funky_chars(self):
+ unless = self.failUnless
+ failif = self.failIf
+ secret = passwords.make_secret(self.pwub, self.scheme)
+ unless(passwords.check_response(secret, self.pwub))
+ failif(passwords.check_response(secret, self.badub))
+
+
+
+class TestBogusPasswords(TestPasswordsBase):
+ scheme = -1
+
+ def test_passwords(self):
+ self.assertRaises(errors.BadPasswordSchemeError,
+ passwords.make_secret, self.pw8a, self.scheme)
+
+ def test_passwords_with_funky_chars(self):
+ self.assertRaises(errors.BadPasswordSchemeError,
+ passwords.make_secret, self.pw8b, self.scheme)
+
+ def test_unicode_passwords_with_funky_chars(self):
+ self.assertRaises(errors.BadPasswordSchemeError,
+ passwords.make_secret, self.pwub, self.scheme)
+
+
+
+class TestNonePasswords(TestPasswordsBase):
+ scheme = passwords.Schemes.no_scheme
+
+ def test_passwords(self):
+ failif = self.failIf
+ secret = passwords.make_secret(self.pw8a, self.scheme)
+ failif(passwords.check_response(secret, self.pw8a))
+ failif(passwords.check_response(secret, self.bad8a))
+
+ def test_passwords_with_funky_chars(self):
+ failif = self.failIf
+ secret = passwords.make_secret(self.pw8b, self.scheme)
+ failif(passwords.check_response(secret, self.pw8b))
+ failif(passwords.check_response(secret, self.bad8b))
+
+ def test_unicode_passwords_with_funky_chars(self):
+ failif = self.failIf
+ secret = passwords.make_secret(self.pwub, self.scheme)
+ failif(passwords.check_response(secret, self.pwub))
+ failif(passwords.check_response(secret, self.badub))
+
+
+
+class TestCleartextPasswords(TestPasswordsBase):
+ scheme = passwords.Schemes.cleartext
+
+
+class TestSHAPasswords(TestPasswordsBase):
+ scheme = passwords.Schemes.sha
+
+
+class TestSSHAPasswords(TestPasswordsBase):
+ scheme = passwords.Schemes.ssha
+
+
+class TestPBKDF2Passwords(TestPasswordsBase):
+ scheme = passwords.Schemes.pbkdf2
+
+
+
+class TestSchemeLookup(unittest.TestCase):
+ def test_scheme_name_lookup(self):
+ unless = self.failUnless
+ unless(passwords.lookup_scheme('NONE') is passwords.Schemes.no_scheme)
+ unless(passwords.lookup_scheme('CLEARTEXT') is
+ passwords.Schemes.cleartext)
+ unless(passwords.lookup_scheme('SHA') is passwords.Schemes.sha)
+ unless(passwords.lookup_scheme('SSHA') is passwords.Schemes.ssha)
+ unless(passwords.lookup_scheme('PBKDF2') is passwords.Schemes.pbkdf2)
+ unless(passwords.lookup_scheme(' -bogus- ') is None)
+
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestBogusPasswords))
+ suite.addTest(unittest.makeSuite(TestNonePasswords))
+ suite.addTest(unittest.makeSuite(TestCleartextPasswords))
+ suite.addTest(unittest.makeSuite(TestSHAPasswords))
+ suite.addTest(unittest.makeSuite(TestSSHAPasswords))
+ suite.addTest(unittest.makeSuite(TestPBKDF2Passwords))
+ suite.addTest(unittest.makeSuite(TestSchemeLookup))
+ return suite
--
cgit v1.2.3-70-g09d2