[Checkins] SVN: zope.password/trunk/ Add a SMD5 (salted MD5) implementation, compatible with OpenLDAP.

Martijn Pieters mj at zopatista.com
Sun Feb 20 17:08:42 EST 2011


Log message for revision 120479:
  Add a SMD5 (salted MD5) implementation, compatible with OpenLDAP.

Changed:
  U   zope.password/trunk/CHANGES.txt
  U   zope.password/trunk/README.txt
  U   zope.password/trunk/src/zope/password/configure.zcml
  U   zope.password/trunk/src/zope/password/password.py
  U   zope.password/trunk/src/zope/password/testing.py

-=-
Modified: zope.password/trunk/CHANGES.txt
===================================================================
--- zope.password/trunk/CHANGES.txt	2011-02-20 21:55:43 UTC (rev 120478)
+++ zope.password/trunk/CHANGES.txt	2011-02-20 22:08:42 UTC (rev 120479)
@@ -16,6 +16,9 @@
 - 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.
 
+- Add a SMD5 (salted MD5) password manager to fully support all encoding
+  schemes implemented by OpenLDAP.
+
 - Add a MySQL PASSWORD() (versions before 4.1) password manager, as also found
   in Zope2's AccessControl.AuthEncoding module.
 

Modified: zope.password/trunk/README.txt
===================================================================
--- zope.password/trunk/README.txt	2011-02-20 21:55:43 UTC (rev 120478)
+++ zope.password/trunk/README.txt	2011-02-20 22:08:42 UTC (rev 120479)
@@ -5,7 +5,7 @@
 This package provides a password manager mechanism. Password manager
 is an utility object that can encode and check encoded
 passwords. Beyond the generic interface, this package also provides
-six implementations:
+seven implementations:
 
 * PlainTextPasswordManager - the most simple and the less secure
   one. It does not do any password encoding and simply checks password
@@ -16,14 +16,18 @@
   encode passwords. It's generally weak against dictionary attacks due to a
   lack of a salt.
  
+* SMD5PasswordManager - a password manager that uses MD5 algorithm, together
+  with a salt to encode passwords. It's better protected against against
+  dictionary attacks, but the MD5 hashing algorithm is not as strong as the
+  SHA1 algorithm.
+
 * SHA1PasswordManager - a password manager that uses SHA1 algorithm to
   encode passwords. It has the same weakness as the MD5PasswordManager.
  
 * SSHAPasswordManager - the most secure password manager that is
   strong against dictionary attacks. It's basically SHA1-encoding
   password manager which also incorporates a salt into the password
-  when encoding it. This password manager is compatible with passwords
-  used in LDAP databases.
+  when encoding it.
 
 * CryptPasswordManager - A manager implementing the crypt(3) hashing scheme.
   Only available if the python crypt module is installed. This is a legacy
@@ -34,6 +38,8 @@
   implemented in the MySQL PASSWORD function in MySQL versions before 4.1. 
   Note that this method results in a very weak 16-byte hash.
 
+The Crypt, MD5, SMD5, SHA and SSHA password managers are all compatible with
+RFC 2307 LDAP implementations of the same password encoding schemes.
 
 It is strongly recommended to use SSHAPasswordManager, as it's the
 most secure.

Modified: zope.password/trunk/src/zope/password/configure.zcml
===================================================================
--- zope.password/trunk/src/zope/password/configure.zcml	2011-02-20 21:55:43 UTC (rev 120478)
+++ zope.password/trunk/src/zope/password/configure.zcml	2011-02-20 22:08:42 UTC (rev 120479)
@@ -16,6 +16,12 @@
       />
 
   <utility
+      name="SMD5"
+      provides=".interfaces.IMatchingPasswordManager"
+      factory=".password.SMD5PasswordManager"
+      />
+
+  <utility
       name="SHA1"
       provides=".interfaces.IMatchingPasswordManager"
       factory=".password.SHA1PasswordManager"
@@ -57,6 +63,10 @@
       <allow interface=".interfaces.IMatchingPasswordManager" />
     </class>
   
+    <class class=".password.SMD5PasswordManager">
+      <allow interface=".interfaces.IMatchingPasswordManager" />
+    </class>
+  
     <class class=".password.SHA1PasswordManager">
       <allow interface=".interfaces.IMatchingPasswordManager" />
     </class>

Modified: zope.password/trunk/src/zope/password/password.py
===================================================================
--- zope.password/trunk/src/zope/password/password.py	2011-02-20 21:55:43 UTC (rev 120478)
+++ zope.password/trunk/src/zope/password/password.py	2011-02-20 22:08:42 UTC (rev 120479)
@@ -182,6 +182,93 @@
         return encoded_password.startswith('{SSHA}')
 
 
+class SMD5PasswordManager(PlainTextPasswordManager):
+    """SMD5 password manager.
+
+    SMD5 is basically SMD5-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.
+
+    >>> from zope.interface.verify import verifyObject
+
+    >>> manager = SMD5PasswordManager()
+    >>> verifyObject(IMatchingPasswordManager, manager)
+    True
+
+    >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
+    >>> encoded = manager.encodePassword(password, salt="")
+    >>> encoded
+    '{SMD5}ht3czsRdtFmfGsAAGOVBOQ=='
+
+    >>> manager.match(encoded)
+    True
+    >>> manager.checkPassword(encoded, password)
+    True
+    >>> manager.checkPassword(encoded, password + u"wrong")
+    False
+
+    Using the `slappasswd` utility to encode ``secret``, we get
+    ``{SMD5}zChC6x0tl2zr9fjvjZzKePV5KWA=`` 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 SMD5::
+
+    >>> from base64 import standard_b64decode
+    >>> salt = standard_b64decode('9XkpYA==')
+    >>> password = 'secret'
+    >>> encoded = manager.encodePassword(password, salt)
+    >>> encoded
+    '{SMD5}zChC6x0tl2zr9fjvjZzKePV5KWA='
+
+    >>> manager.checkPassword(encoded, password)
+    True
+    >>> manager.checkPassword(encoded, password + u"wrong")
+    False
+
+    Because a random salt is generated, the output of encodePassword is
+    different every time you call it.
+
+    >>> 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
+
+    The manager only claims to implement SMD5 encodings, anything not starting
+    with the string {SMD5} returns False::
+
+    >>> manager.match('{MD5}someotherhash')
+    False
+
+    """
+
+    def encodePassword(self, password, salt=None):
+        if salt is None:
+            salt = urandom(4)
+        hash = md5(_encoder(password)[0])
+        hash.update(salt)
+        return '{SMD5}' + standard_b64encode(hash.digest() + salt)
+
+    def checkPassword(self, encoded_password, password):
+        # standard_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')[6:]
+        byte_string = standard_b64decode(encoded_password)
+        salt = byte_string[16:]
+        return encoded_password == self.encodePassword(password, salt)[6:]
+
+    def match(self, encoded_password):
+        return encoded_password.startswith('{SMD5}')
+
+
 class MD5PasswordManager(PlainTextPasswordManager):
     """MD5 password manager.
 
@@ -347,6 +434,7 @@
 managers = [
     ('Plain Text', PlainTextPasswordManager()),
     ('MD5', MD5PasswordManager()),
+    ('SMD5', SMD5PasswordManager()),
     ('SHA1', SHA1PasswordManager()),
     ('SSHA', SSHAPasswordManager()),
 ]

Modified: zope.password/trunk/src/zope/password/testing.py
===================================================================
--- zope.password/trunk/src/zope/password/testing.py	2011-02-20 21:55:43 UTC (rev 120478)
+++ zope.password/trunk/src/zope/password/testing.py	2011-02-20 22:08:42 UTC (rev 120479)
@@ -21,6 +21,7 @@
 from zope.password.interfaces import IMatchingPasswordManager
 from zope.password.password import PlainTextPasswordManager
 from zope.password.password import MD5PasswordManager
+from zope.password.password import SMD5PasswordManager
 from zope.password.password import SHA1PasswordManager
 from zope.password.password import SSHAPasswordManager
 from zope.password.legacy import MySQLPasswordManager
@@ -42,6 +43,8 @@
     <zope.password.password.PlainTextPasswordManager object at 0x...>
     >>> getUtility(IMatchingPasswordManager, 'SSHA')
     <zope.password.password.SSHAPasswordManager object at 0x...>
+    >>> getUtility(IMatchingPasswordManager, 'SMD5')
+    <zope.password.password.SMD5PasswordManager object at 0x...>
     >>> getUtility(IMatchingPasswordManager, 'MD5')
     <zope.password.password.MD5PasswordManager object at 0x...>
     >>> getUtility(IMatchingPasswordManager, 'SHA1')
@@ -71,6 +74,8 @@
     True
     >>> 'MD5' in voc
     True
+    >>> 'SMD5' in voc
+    True
     >>> 'MYSQL' in voc
     True
 
@@ -82,6 +87,7 @@
                    'Plain Text')
     provideUtility(SSHAPasswordManager(), IMatchingPasswordManager, 'SSHA')
     provideUtility(MD5PasswordManager(), IMatchingPasswordManager, 'MD5')
+    provideUtility(SMD5PasswordManager(), IMatchingPasswordManager, 'SMD5')
     provideUtility(SHA1PasswordManager(), IMatchingPasswordManager, 'SHA1')
     provideUtility(MySQLPasswordManager(), IMatchingPasswordManager, 'MYSQL')
     



More information about the checkins mailing list