From 25b407e24fe21dc46a4f9efa77734d55e0ed4bdd Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 8 Apr 2011 22:02:49 -0400 Subject: encrypt_password(): New convenience function for ensuring that a password is both encrypted according to a scheme, and a bytes object. add_member(): Use encrypt_password(). cli_members: Give the user a default, user-friendly password. Of course, this will be encrypted so it can't be retrieved, but it can be reset. Passwords are stored as bytes objects, not unicode now. ConfigLayer: Set the default test password scheme to cleartext. General test repair. --- src/mailman/utilities/passwords.py | 41 +++++++++++++++++++++++++-- src/mailman/utilities/tests/test_passwords.py | 34 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) (limited to 'src/mailman/utilities') diff --git a/src/mailman/utilities/passwords.py b/src/mailman/utilities/passwords.py index 0de4255da..896872436 100644 --- a/src/mailman/utilities/passwords.py +++ b/src/mailman/utilities/passwords.py @@ -26,6 +26,7 @@ __metaclass__ = type __all__ = [ 'Schemes', 'check_response', + 'encrypt_password', 'make_secret', 'make_user_friendly_password', ] @@ -50,6 +51,7 @@ from mailman.core import errors SALT_LENGTH = 20 # bytes ITERATIONS = 2000 EMPTYSTRING = '' +SCHEME_RE = r'{(?P[^}]+?)}(?P.*)' @@ -294,8 +296,7 @@ def check_response(challenge, response): :return: True if the response matches the challenge. :rtype: bool """ - mo = re.match(r'{(?P[^}]+?)}(?P.*)', - challenge, re.IGNORECASE) + mo = re.match(SCHEME_RE, challenge, re.IGNORECASE) if not mo: return False # See above for why we convert here. However because we should have @@ -323,6 +324,42 @@ def lookup_scheme(scheme_name): return _SCHEMES_BY_TAG.get(scheme_name.lower()) +def encrypt_password(password, scheme=None): + """Return an encrypted password. + + If the given password is already encrypted (i.e. it has a scheme prefix), + then the password is return unchanged. Otherwise, it is encrypted with + the given scheme or the default scheme. + + :param password: The plain text or encrypted password. + :type password: string + :param scheme: The scheme enum to use for encryption. If not given, the + system default scheme is used. This can be a `Schemes` enum item, or + the scheme name as a string. + :type scheme: `Schemes` enum, or string. + :return: The encrypted password. + :rtype: bytes + """ + if not isinstance(password, (bytes, unicode)): + raise ValueError('Got {0}, expected unicode or bytes'.format( + type(password))) + if re.match(SCHEME_RE, password, re.IGNORECASE): + # Just ensure we're getting bytes back. + if isinstance(password, unicode): + return password.encode('us-ascii') + assert isinstance(password, bytes), 'Expected bytes' + return password + if scheme is None: + password_scheme = lookup_scheme(config.passwords.password_scheme) + elif scheme in Schemes: + password_scheme = scheme + else: + password_scheme = lookup_scheme(scheme) + if password_scheme is None: + raise ValueError('Bad password scheme: {0}'.format(scheme)) + return make_secret(password, password_scheme) + + # Password generation. diff --git a/src/mailman/utilities/tests/test_passwords.py b/src/mailman/utilities/tests/test_passwords.py index 7b6989779..c9b3d2e91 100644 --- a/src/mailman/utilities/tests/test_passwords.py +++ b/src/mailman/utilities/tests/test_passwords.py @@ -170,6 +170,40 @@ class TestPasswordGeneration(unittest.TestCase): self.assertTrue(vowel in 'aeiou', vowel) self.assertTrue(consonant not in 'aeiou', consonant) + def test_encrypt_password_plaintext_default_scheme(self): + # Test that a plain text password gets encrypted. + self.assertEqual(passwords.encrypt_password('abc'), + '{CLEARTEXT}abc') + + def test_encrypt_password_plaintext(self): + # Test that a plain text password gets encrypted with the given scheme. + scheme = passwords.Schemes.sha + self.assertEqual(passwords.encrypt_password('abc', scheme), + '{SHA}qZk-NkcGgWq6PiVxeFDCbJzQ2J0=') + + def test_encrypt_password_plaintext_by_scheme_name(self): + # Test that a plain text password gets encrypted with the given + # scheme, which is given by name. + self.assertEqual(passwords.encrypt_password('abc', 'cleartext'), + '{CLEARTEXT}abc') + + def test_encrypt_password_already_encrypted_default_scheme(self): + # Test that a password which is already encrypted is return unchanged. + self.assertEqual(passwords.encrypt_password('{SHA}abc'), '{SHA}abc') + + def test_encrypt_password_already_encrypted(self): + # Test that a password which is already encrypted is return unchanged, + # ignoring any requested scheme. + scheme = passwords.Schemes.cleartext + self.assertEqual(passwords.encrypt_password('{SHA}abc', scheme), + '{SHA}abc') + + def test_encrypt_password_password_value_error(self): + self.assertRaises(ValueError, passwords.encrypt_password, 7) + + def test_encrypt_password_scheme_value_error(self): + self.assertRaises(ValueError, passwords.encrypt_password, 'abc', 'foo') + def test_suite(): -- cgit v1.2.3-70-g09d2