[Checkins] SVN: zope.app.authentication/branches/3.4/src/zope/app/authentication/p backport the SSHA password manager to the 3.4 line

Jan-Wijbrand Kolman janwijbrand at gmail.com
Thu May 27 05:57:39 EDT 2010


Log message for revision 112760:
  backport the SSHA password manager to the 3.4 line

Changed:
  U   zope.app.authentication/branches/3.4/src/zope/app/authentication/password.py
  U   zope.app.authentication/branches/3.4/src/zope/app/authentication/placelesssetup.py

-=-
Modified: zope.app.authentication/branches/3.4/src/zope/app/authentication/password.py
===================================================================
--- zope.app.authentication/branches/3.4/src/zope/app/authentication/password.py	2010-05-27 09:54:47 UTC (rev 112759)
+++ zope.app.authentication/branches/3.4/src/zope/app/authentication/password.py	2010-05-27 09:57:39 UTC (rev 112760)
@@ -17,6 +17,11 @@
 """
 __docformat__ = 'restructuredtext'
 
+from base64 import urlsafe_b64encode
+from base64 import urlsafe_b64decode
+from os import urandom
+from random import randint
+from codecs import getencoder
 try:
     from hashlib import md5, sha1
 except ImportError:
@@ -24,16 +29,12 @@
     from md5 import new as md5
     from sha import new as sha1
 
-from random import randint
-from codecs import getencoder
-
 from zope.interface import implements, classProvides
 from zope.schema.interfaces import IVocabularyFactory
 from zope.app.component.vocabulary import UtilityVocabulary
 
 from zope.app.authentication.interfaces import IPasswordManager
 
-
 _encoder = getencoder("utf-8")
 
 
@@ -61,13 +62,94 @@
     def encodePassword(self, password):
         return password
 
-    def checkPassword(self, storedPassword, password):
-        return storedPassword == self.encodePassword(password)
+    def checkPassword(self, encoded_password, password):
+        return encoded_password == self.encodePassword(password)
 
 
+class SSHAPasswordManager(PlainTextPasswordManager):
+    """SSHA password manager.
+
+    SSHA is basically SHA1-encoding which also incorporates a salt
+    into the encoded string. This way, stored passwords are more
+    robust against dictionary attacks of attackers that could get
+    access to lists of encoded passwords.
+
+    SSHA is regularly used in LDAP databases and we should be
+    compatible with passwords used there.
+
+    >>> from zope.interface.verify import verifyObject
+
+    >>> manager = SSHAPasswordManager()
+    >>> verifyObject(IPasswordManager, manager)
+    True
+
+    >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
+    >>> encoded = manager.encodePassword(password, salt="")
+    >>> encoded
+    '{SSHA}BLTuxxVMXzouxtKVb7gLgNxzdAI='
+
+    >>> manager.checkPassword(encoded, password)
+    True
+    >>> manager.checkPassword(encoded, password + u"wrong")
+    False
+
+    Using the `slappasswd` utility to encode ``secret``, we get
+    ``{SSHA}J4mrr3NQHXzLVaT0h9TuEWoJOrxeQ5lv`` 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 SSHA::
+
+    >>> from base64 import urlsafe_b64decode
+    >>> salt = urlsafe_b64decode('XkOZbw==')
+    >>> encoded = manager.encodePassword('secret', salt)
+    >>> encoded
+    '{SSHA}J4mrr3NQHXzLVaT0h9TuEWoJOrxeQ5lv'
+
+    >>> encoded = manager.encodePassword(password)
+    >>> manager.checkPassword(encoded, password)
+    True
+    >>> manager.checkPassword(encoded, password + u"wrong")
+    False
+
+    >>> manager.encodePassword(password) != manager.encodePassword(password)
+    True
+
+    The password manager should be able to cope with unicode strings for input::
+
+    >>> passwd = u'foobar\u2211' # sigma-sign.
+    >>> manager.checkPassword(manager.encodePassword(passwd), passwd)
+    True
+    >>> manager.checkPassword(unicode(manager.encodePassword(passwd)), passwd)
+    True
+
+    """
+
+    implements(IPasswordManager)
+
+    def encodePassword(self, password, salt=None):
+        if salt is None:
+            salt = urandom(4)
+        hash = sha1(_encoder(password)[0])
+        hash.update(salt)
+        return '{SSHA}' + urlsafe_b64encode(hash.digest() + salt)
+
+    def checkPassword(self, encoded_password, password):
+        # urlsafe_b64decode() cannot handle unicode input string. We
+        # encode to ascii. This is safe as the encoded_password string
+        # should not contain non-ascii characters anyway.
+        encoded_password = encoded_password.encode('ascii')
+        byte_string = urlsafe_b64decode(encoded_password[6:])
+        salt = byte_string[20:]
+        return encoded_password == self.encodePassword(password, salt)
+
+
 class MD5PasswordManager(PlainTextPasswordManager):
     """MD5 password manager.
 
+    Note: use of salt in this password manager is purely
+    cosmetical. Use SSHA if you want increased security.
+
     >>> from zope.interface.verify import verifyObject
 
     >>> manager = MD5PasswordManager()
@@ -77,7 +159,7 @@
     >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
     >>> encoded = manager.encodePassword(password, salt="")
     >>> encoded
-    '86dddccec45db4599f1ac00018e54139'
+    '{MD5}86dddccec45db4599f1ac00018e54139'
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -93,6 +175,17 @@
 
     >>> manager.encodePassword(password) != manager.encodePassword(password)
     True
+
+    The old version of this password manager didn't add the {MD5} to
+    passwords. Let's check if it can work with old stored passwords.
+
+    >>> encoded = manager.encodePassword(password, salt="")
+    >>> encoded = encoded[5:]
+    >>> encoded
+    '86dddccec45db4599f1ac00018e54139'
+
+    >>> manager.checkPassword(encoded, password)
+    True
     """
 
     implements(IPasswordManager)
@@ -100,16 +193,22 @@
     def encodePassword(self, password, salt=None):
         if salt is None:
             salt = "%08x" % randint(0, 0xffffffff)
-        return salt + md5(_encoder(password)[0]).hexdigest()
+        return '{MD5}%s%s' % (salt, md5(_encoder(password)[0]).hexdigest())
 
-    def checkPassword(self, storedPassword, password):
-        salt = storedPassword[:-32]
-        return storedPassword == self.encodePassword(password, salt)
+    def checkPassword(self, encoded_password, password):
+        if encoded_password.startswith('{MD5}'):
+            salt = encoded_password[5:-32]
+            return encoded_password == self.encodePassword(password, salt)
+        salt = encoded_password[:-32]
+        return encoded_password == self.encodePassword(password, salt)[5:]
 
 
 class SHA1PasswordManager(PlainTextPasswordManager):
     """SHA1 password manager.
 
+    Note: use of salt in this password manager is purely
+    cosmetical. Use SSHA if you want increased security.
+
     >>> from zope.interface.verify import verifyObject
 
     >>> manager = SHA1PasswordManager()
@@ -119,7 +218,7 @@
     >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
     >>> encoded = manager.encodePassword(password, salt="")
     >>> encoded
-    '04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+    '{SHA1}04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -135,6 +234,18 @@
 
     >>> manager.encodePassword(password) != manager.encodePassword(password)
     True
+
+    The old version of this password manager didn't add the {SHA1} to
+    passwords. Let's check if it can work with old stored passwords.
+
+    >>> encoded = manager.encodePassword(password, salt="")
+    >>> encoded = encoded[6:]
+    >>> encoded
+    '04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+
+    >>> manager.checkPassword(encoded, password)
+    True
+
     """
 
     implements(IPasswordManager)
@@ -142,11 +253,14 @@
     def encodePassword(self, password, salt=None):
         if salt is None:
             salt = "%08x" % randint(0, 0xffffffff)
-        return salt + sha1(_encoder(password)[0]).hexdigest()
+        return '{SHA1}%s%s' % (salt, sha1(_encoder(password)[0]).hexdigest())
 
-    def checkPassword(self, storedPassword, password):
-        salt = storedPassword[:-40]
-        return storedPassword == self.encodePassword(password, salt)
+    def checkPassword(self, encoded_password, password):
+        if encoded_password.startswith('{SHA1}'):
+            salt = encoded_password[6:-40]
+            return encoded_password == self.encodePassword(password, salt)
+        salt = encoded_password[:-40]
+        return encoded_password == self.encodePassword(password, salt)[6:]
 
 
 # Simple registry used by mkzopeinstance script
@@ -154,6 +268,7 @@
     ("Plain Text", PlainTextPasswordManager()), # default
     ("MD5", MD5PasswordManager()),
     ("SHA1", SHA1PasswordManager()),
+    ("SSHA", SSHAPasswordManager()),
 ]
 
 

Modified: zope.app.authentication/branches/3.4/src/zope/app/authentication/placelesssetup.py
===================================================================
--- zope.app.authentication/branches/3.4/src/zope/app/authentication/placelesssetup.py	2010-05-27 09:54:47 UTC (rev 112759)
+++ zope.app.authentication/branches/3.4/src/zope/app/authentication/placelesssetup.py	2010-05-27 09:57:39 UTC (rev 112760)
@@ -22,8 +22,8 @@
 from zope.app.authentication.password import PlainTextPasswordManager
 from zope.app.authentication.password import MD5PasswordManager
 from zope.app.authentication.password import SHA1PasswordManager
+from zope.app.authentication.password import SSHAPasswordManager
 
-
 class PlacelessSetup(object):
 
     def setUp(self):
@@ -31,3 +31,4 @@
             "Plain Text")
         ztapi.provideUtility(IPasswordManager, MD5PasswordManager(), "MD5")
         ztapi.provideUtility(IPasswordManager, SHA1PasswordManager(), "SHA1")
+        ztapi.provideUtility(IPasswordManager, SHA1PasswordManager(), "SSHA")
\ No newline at end of file



More information about the checkins mailing list