diff --git a/CHANGES.txt b/CHANGES.txt
index 224f320..eb87dba 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,6 +12,9 @@ CHANGES
- Use {SHA} as the prefix for SHA1-encoded passwords to be compatible with
RFC 2307, but support matching against {SHA1} for backwards compatibility.
+- Add a crypt password manager to fully support all methods named in RFC 2307.
+ It is contained in the 'legacy' module however, to flag crypt's status.
+
3.6.1 (2010-05-27)
------------------
diff --git a/src/zope/password/configure.zcml b/src/zope/password/configure.zcml
index 4bc0130..a788b08 100644
--- a/src/zope/password/configure.zcml
+++ b/src/zope/password/configure.zcml
@@ -27,6 +27,14 @@
factory=".password.SSHAPasswordManager"
/>
+
+
+
+
+
+
+
+
diff --git a/src/zope/password/legacy.py b/src/zope/password/legacy.py
new file mode 100644
index 0000000..d1037ea
--- /dev/null
+++ b/src/zope/password/legacy.py
@@ -0,0 +1,114 @@
+##############################################################################
+#
+# Copyright (c) 2009 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Legacy password managers, using now-outdated, insecure methods for hashing
+"""
+__docformat__ = 'restructuredtext'
+
+from codecs import getencoder
+
+try:
+ from crypt import crypt
+ from random import choice
+except ImportError:
+ # The crypt module is not universally available, apparently
+ crypt = None
+
+from zope.interface import implements
+from zope.password.interfaces import IPasswordManager
+
+_encoder = getencoder("utf-8")
+
+
+if crypt is not None:
+ class CryptPasswordManager(object):
+ """Crypt password manager.
+
+ Implements a UNIX crypt(3) hashing scheme. Note that crypt is
+ considered far inferior to more modern schemes such as SSHA hashing,
+ and only uses the first 8 characters of a password.
+
+ >>> from zope.interface.verify import verifyObject
+
+ >>> manager = CryptPasswordManager()
+ >>> verifyObject(IPasswordManager, manager)
+ True
+
+ >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
+ >>> encoded = manager.encodePassword(password, salt="..")
+ >>> encoded
+ '{CRYPT}..I1I8wps4Na2'
+ >>> manager.match(encoded)
+ True
+ >>> manager.checkPassword(encoded, password)
+ True
+
+ Unfortunately, crypt only looks at the first 8 characters, so matching
+ against an 8 character password plus suffix always matches. Our test
+ password (including utf-8 encoding) is exactly 8 characters long, and
+ thus affixing 'wrong' to it tests as a correct password::
+
+ >>> manager.checkPassword(encoded, password + u"wrong")
+ True
+
+ Using a completely different password is rejected as expected::
+
+ >>> manager.checkPassword(encoded, 'completely wrong')
+ False
+
+ Using the `openssl passwd` command-line utility to encode ``secret``,
+ we get ``erz50QD3gv4Dw`` as seeded hash.
+
+ Our password manager generates the same value when seeded with the
+ same salt, so we can be sure, our output is compatible with
+ standard LDAP tools that also use crypt::
+
+ >>> salt = 'er'
+ >>> password = 'secret'
+ >>> encoded = manager.encodePassword(password, salt)
+ >>> encoded
+ '{CRYPT}erz50QD3gv4Dw'
+
+ >>> manager.checkPassword(encoded, password)
+ True
+ >>> manager.checkPassword(encoded, password + u"wrong")
+ False
+
+ >>> manager.encodePassword(password) != manager.encodePassword(password)
+ True
+
+ The manager only claims to implement CRYPT encodings, anything not
+ starting with the string {CRYPT} returns False::
+
+ >>> manager.match('{MD5}someotherhash')
+ False
+
+ """
+
+ implements(IPasswordManager)
+
+ def encodePassword(self, password, salt=None):
+ if salt is None:
+ choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789./")
+ salt = choice(choices) + choice(choices)
+ return '{CRYPT}%s' % crypt(_encoder(password)[0], salt)
+
+ def checkPassword(self, encoded_password, password):
+ return encoded_password == self.encodePassword(password,
+ encoded_password[7:9])
+
+ def match(self, encoded_password):
+ return encoded_password.startswith('{CRYPT}')
+
diff --git a/src/zope/password/testing.py b/src/zope/password/testing.py
index b37229a..93ceca6 100644
--- a/src/zope/password/testing.py
+++ b/src/zope/password/testing.py
@@ -25,6 +25,11 @@
from zope.password.password import SSHAPasswordManager
from zope.password.vocabulary import PasswordManagerNamesVocabulary
+try:
+ from zope.password.legacy import CryptPasswordManager
+except ImportError:
+ CryptPasswordManager = None
+
def setUpPasswordManagers():
"""Helper function for setting up password manager utilities for tests
@@ -41,6 +46,16 @@ def setUpPasswordManagers():
>>> getUtility(IPasswordManager, 'SHA1')
+ >>> try:
+ ... import crypt
+ ... except ImportError:
+ ... CryptPasswordManager = None
+ ... True
+ ... else:
+ ... from zope.password.legacy import CryptPasswordManager
+ ... getUtility(IPasswordManager, 'Crypt') is CryptPasswordManager
+ True
+
>>> voc = getUtility(IVocabularyFactory, 'Password Manager Names')
>>> voc = voc(None)
>>> voc
@@ -53,12 +68,18 @@ def setUpPasswordManagers():
True
>>> 'MD5' in voc
True
+
+ >>> CryptPasswordManager is None or 'Crypt' in voc
+ True
"""
provideUtility(PlainTextPasswordManager(), IPasswordManager, 'Plain Text')
provideUtility(SSHAPasswordManager(), IPasswordManager, 'SSHA')
provideUtility(MD5PasswordManager(), IPasswordManager, 'MD5')
provideUtility(SHA1PasswordManager(), IPasswordManager, 'SHA1')
+
+ if CryptPasswordManager is not None:
+ provideUtility(CryptPasswordManager, IPasswordManager, 'Crypt')
provideUtility(PasswordManagerNamesVocabulary,
IVocabularyFactory, 'Password Manager Names')
diff --git a/src/zope/password/tests/test_password.py b/src/zope/password/tests/test_password.py
index c631d85..2badd03 100644
--- a/src/zope/password/tests/test_password.py
+++ b/src/zope/password/tests/test_password.py
@@ -19,6 +19,7 @@
def test_suite():
return unittest.TestSuite((
doctest.DocTestSuite('zope.password.password'),
+ doctest.DocTestSuite('zope.password.legacy'),
doctest.DocTestSuite(
'zope.password.testing',
optionflags=doctest.ELLIPSIS),