[Checkins] SVN: zope.password/trunk/ Make SHA and MD5 output compatible with LDAP schemes.

Martijn Pieters mj at zopatista.com
Sun Feb 20 14:11:39 EST 2011


Log message for revision 120473:
  Make SHA and MD5 output compatible with LDAP schemes.
  
  This means using base64 output instead of hexdigests. We still support checking passwords against the old format.

Changed:
  U   zope.password/trunk/CHANGES.txt
  U   zope.password/trunk/src/zope/password/password.py

-=-
Modified: zope.password/trunk/CHANGES.txt
===================================================================
--- zope.password/trunk/CHANGES.txt	2011-02-20 15:45:38 UTC (rev 120472)
+++ zope.password/trunk/CHANGES.txt	2011-02-20 19:11:38 UTC (rev 120473)
@@ -19,10 +19,11 @@
 - Add a MySQL PASSWORD() (versions before 4.1) password manager, as also found
   in Zope2's AccessControl.AuthEncoding module.
 
-- Remove the useless, cosmetic salt from the MD5 and SHA1 password managers.
-  This makes the output of these managers compatible with other MD5 and SHA1
-  hash implementations such as RFC 2307 but doesn't lower it's security in any
-  way. Checking passwards against old, still 'salted' password hashes is still
+- Remove the useless, cosmetic salt from the MD5 and SHA1 password managers,
+  and use base64 encoding instead of hexdigests. This makes the output of
+  these managers compatible with other MD5 and SHA1 hash implementations such
+  as RFC 2307 but doesn't lower it's security in any way. Checking passwards
+  against old, still 'salted' password hashes with hexdigests is still
   supported.
 
 - Use the standard_base64encode method instead of url_base64encode to maintain

Modified: zope.password/trunk/src/zope/password/password.py
===================================================================
--- zope.password/trunk/src/zope/password/password.py	2011-02-20 15:45:38 UTC (rev 120472)
+++ zope.password/trunk/src/zope/password/password.py	2011-02-20 19:11:38 UTC (rev 120473)
@@ -18,6 +18,7 @@
 from base64 import standard_b64encode
 from base64 import standard_b64decode
 from base64 import urlsafe_b64decode
+from binascii import a2b_hex
 from os import urandom
 from codecs import getencoder
 try:
@@ -190,7 +191,7 @@
     >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
     >>> encoded = manager.encodePassword(password)
     >>> encoded
-    '{MD5}86dddccec45db4599f1ac00018e54139'
+    '{MD5}ht3czsRdtFmfGsAAGOVBOQ=='
     >>> manager.match(encoded)
     True
     >>> manager.checkPassword(encoded, password)
@@ -198,14 +199,21 @@
     >>> manager.checkPassword(encoded, password + u"wrong")
     False
 
-    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.
+    This password manager is compatible with other RFC 2307 MD5
+    implementations. For example the output of the slappasswd command for
+    a MD5 hashing of ``secret`` is ``{MD5}Xr4ilOzQ4PCOq3aQ0qbuaQ==``,
+    and our implementation returns the same hash::
 
-    >>> encoded = manager.encodePassword(password)
-    >>> encoded = encoded[5:]
-    >>> encoded
-    '86dddccec45db4599f1ac00018e54139'
+    >>> manager.encodePassword('secret')
+    '{MD5}Xr4ilOzQ4PCOq3aQ0qbuaQ=='
 
+    A previous version of this manager also created a cosmetic salt, added
+    to the start of the hash, but otherwise not used in creating the hash
+    itself. Moreover, it generated the MD5 hash as a hex digest, not a base64
+    encoded value and did not include the {MD5} prefix. Such hashed values are
+    still supported too:
+
+    >>> encoded = 'salt86dddccec45db4599f1ac00018e54139'
     >>> manager.checkPassword(encoded, password)
     True
 
@@ -215,23 +223,20 @@
     >>> manager.match(encoded)
     False
 
-    A previous version of this manager also created a cosmetic salt, added
-    to the start of the hash, but otherwise not used in creating the hash
-    itself. To still support these 'hashed' passwords, only the last 32 bytes
-    of the pre-existing hash are used:
-    
-    >>> manager.checkPassword('salt' + encoded, password)
-    True
-
     """
 
     def encodePassword(self, password, salt=None):
         # The salt argument only exists for backwards compatibility and is
         # ignored on purpose.
-        return '{MD5}%s' % (md5(_encoder(password)[0]).hexdigest())
+        return '{MD5}%s' % standard_b64encode(
+            md5(_encoder(password)[0]).digest())
 
     def checkPassword(self, encoded_password, password):
-        return encoded_password[-32:] == self.encodePassword(password)[-32:]
+        encoded = encoded_password[encoded_password.find('}') + 1:]
+        if len(encoded) > 24:
+            # Backwards compatible, hexencoded md5 and bogus salt
+            encoded = standard_b64encode(a2b_hex(encoded[-32:]))
+        return encoded == self.encodePassword(password)[5:]
 
     def match(self, encoded_password):
         return encoded_password.startswith('{MD5}')
@@ -249,7 +254,7 @@
     >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
     >>> encoded = manager.encodePassword(password)
     >>> encoded
-    '{SHA}04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+    '{SHA}BLTuxxVMXzouxtKVb7gLgNxzdAI='
     >>> manager.match(encoded)
     True
     >>> manager.checkPassword(encoded, password)
@@ -257,14 +262,21 @@
     >>> manager.checkPassword(encoded, password + u"wrong")
     False
 
-    The old version of this password manager didn't add the {SHA} to
-    passwords. Let's check if it can work with old stored passwords.
+    This password manager is compatible with other RFC 2307 SHA
+    implementations. For example the output of the slappasswd command for
+    a SHA hashing of ``secret`` is ``{SHA}5en6G6MezRroT3XKqkdPOmY/BfQ=``,
+    and our implementation returns the same hash::
 
-    >>> encoded = manager.encodePassword(password)
-    >>> encoded = encoded[5:]
-    >>> encoded
-    '04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+    >>> manager.encodePassword('secret')
+    '{SHA}5en6G6MezRroT3XKqkdPOmY/BfQ='
 
+    A previous version of this manager also created a cosmetic salt, added
+    to the start of the hash, but otherwise not used in creating the hash
+    itself. Moreover, it generated the SHA hash as a hex digest, not a base64
+    encoded value and did not include the {SHA} prefix. Such hashed values are
+    still supported too:
+
+    >>> encoded = 'salt04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
     >>> manager.checkPassword(encoded, password)
     True
 
@@ -274,17 +286,9 @@
     >>> manager.match(encoded)
     False
 
-    A previous version of this manager also created a cosmetic salt, added
-    to the start of the hash, but otherwise not used in creating the hash
-    itself. To still support these 'hashed' passwords, only the last 40 bytes
-    of the pre-existing hash are used:
-    
-    >>> manager.checkPassword('salt' + encoded, password)
-    True
-
     Previously, this password manager used {SHA1} as a prefix, but this was
     changed to be compatible with LDAP (RFC 2307). The old prefix is still
-    supported:
+    supported (note the hexdigest encoding as well):
 
     >>> password = u"right \N{CYRILLIC CAPITAL LETTER A}"
     >>> encoded = '{SHA1}04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
@@ -300,13 +304,19 @@
     def encodePassword(self, password, salt=None):
         # The salt argument only exists for backwards compatibility and is
         # ignored on purpose.
-        return '{SHA}%s' % sha1(_encoder(password)[0]).hexdigest()
+        return '{SHA}%s' % standard_b64encode(
+            sha1(_encoder(password)[0]).digest())
 
     def checkPassword(self, encoded_password, password):
         if self.match(encoded_password):
             encoded = encoded_password[encoded_password.find('}') + 1:]
-            return encoded[-40:] == self.encodePassword(password)[5:]
-        return encoded_password[-40:] == self.encodePassword(password)[5:]
+            if len(encoded) > 28:
+                # Backwards compatible, hexencoded sha1 and bogus salt
+                encoded = standard_b64encode(a2b_hex(encoded[-40:]))
+            return encoded == self.encodePassword(password)[5:]
+        # Backwards compatible, hexdigest and no prefix
+        encoded_password = standard_b64encode(a2b_hex(encoded_password[-40:]))
+        return encoded_password == self.encodePassword(password)[5:]
 
     def match(self, encoded_password):
         return (



More information about the checkins mailing list