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