[Checkins] SVN: zope.password/trunk/ Add a 'match' method to the IPasswordManager interface, which returns True if a given password hash was encdoded with the scheme implemented by the specific manager.

Martijn Pieters mj at zopatista.com
Sun Feb 20 06:23:47 EST 2011


Log message for revision 120458:
  Add a 'match' method to the IPasswordManager interface, which returns True if a given password hash was encdoded with the scheme implemented by the specific manager.
  
  Note that the plain-text manager always returns False for this method, as the alternative is to always return True and thus also validate hashed password against their literal values, a security risk.

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

-=-
Modified: zope.password/trunk/CHANGES.txt
===================================================================
--- zope.password/trunk/CHANGES.txt	2011-02-20 10:39:28 UTC (rev 120457)
+++ zope.password/trunk/CHANGES.txt	2011-02-20 11:23:47 UTC (rev 120458)
@@ -2,10 +2,12 @@
 CHANGES
 =======
 
-3.6.2 (unreleased)
+4.0.0 (unreleased)
 ------------------
 
-- Nothing changed yet.
+- Add a 'match' method to the IPasswordManager interface, which returns True
+  if a given password hash was encdoded with the scheme implemented by the
+  specific manager.
 
 
 3.6.1 (2010-05-27)

Modified: zope.password/trunk/README.txt
===================================================================
--- zope.password/trunk/README.txt	2011-02-20 10:39:28 UTC (rev 120457)
+++ zope.password/trunk/README.txt	2011-02-20 11:23:47 UTC (rev 120458)
@@ -39,7 +39,7 @@
 
 It's very easy to use password managers. The
 ``zope.password.interfaces.IPasswordManager`` interface defines only
-two methods::
+three methods::
 
   def encodePassword(password):
       """Return encoded data for the given password"""
@@ -47,6 +47,13 @@
   def checkPassword(encoded_password, password):
       """Return whether the given encoded data coincide with the given password"""
 
+  def match(encoded_password):
+      """
+      Returns True when the given data was encoded with the scheme
+      implemented by this password manager.
+
+      """
+
 The implementations mentioned above are in the
 ``zope.password.password`` module.
 

Modified: zope.password/trunk/setup.py
===================================================================
--- zope.password/trunk/setup.py	2011-02-20 10:39:28 UTC (rev 120457)
+++ zope.password/trunk/setup.py	2011-02-20 11:23:47 UTC (rev 120458)
@@ -17,7 +17,7 @@
 
 
 setup(name='zope.password',
-      version='3.6.2dev',
+      version='4.0.0dev',
       author='Zope Foundation and Contributors',
       author_email='zope-dev at zope.org',
       description='Password encoding and checking utilities',

Modified: zope.password/trunk/src/zope/password/interfaces.py
===================================================================
--- zope.password/trunk/src/zope/password/interfaces.py	2011-02-20 10:39:28 UTC (rev 120457)
+++ zope.password/trunk/src/zope/password/interfaces.py	2011-02-20 11:23:47 UTC (rev 120458)
@@ -23,3 +23,10 @@
 
     def checkPassword(encoded_password, password):
         """Does the given encoded data coincide with the given password"""
+
+    def match(encoded_password):
+        """
+        Returns True when the given data was encoded with the scheme
+        implemented by this password manager.
+        
+        """

Modified: zope.password/trunk/src/zope/password/password.py
===================================================================
--- zope.password/trunk/src/zope/password/password.py	2011-02-20 10:39:28 UTC (rev 120457)
+++ zope.password/trunk/src/zope/password/password.py	2011-02-20 11:23:47 UTC (rev 120458)
@@ -50,6 +50,15 @@
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
     False
+
+    The plain text password manager *never* claims to implement the scheme,
+    because this would open a security hole, where a hash from a different
+    scheme could be used as-is as a plain-text password. Authentication code
+    that needs to support plain-text passwords need to explicitly check for
+    plain-text password matches after all other options have been tested for::
+
+    >>> manager.match(encoded)
+    False
     """
 
     implements(IPasswordManager)
@@ -60,7 +69,15 @@
     def checkPassword(self, encoded_password, password):
         return encoded_password == self.encodePassword(password)
 
+    def match(self, encoded_password):
+        # We always return False for PlainText because it was a) not encrypted
+        # and b) matching against actual encryption methods would result in
+        # the ability to authenticate with the un-encrypted hash as a password.
+        # For example, you should not be able to authenticate with a literal
+        # SSHA hash.
+        return False
 
+
 class SSHAPasswordManager(PlainTextPasswordManager):
     """SSHA password manager.
 
@@ -83,6 +100,8 @@
     >>> encoded
     '{SSHA}BLTuxxVMXzouxtKVb7gLgNxzdAI='
 
+    >>> manager.match(encoded)
+    True
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -118,6 +137,12 @@
     >>> manager.checkPassword(unicode(manager.encodePassword(passwd)), passwd)
     True
 
+    The manager only claims to implement SSHA encodings, anything not starting
+    with the string {SSHA} returns False::
+
+    >>> manager.match('{MD5}someotherhash')
+    False
+
     """
 
     implements(IPasswordManager)
@@ -138,7 +163,10 @@
         salt = byte_string[20:]
         return encoded_password == self.encodePassword(password, salt)
 
+    def match(self, encoded_password):
+        return encoded_password.startswith('{SSHA}')
 
+
 class MD5PasswordManager(PlainTextPasswordManager):
     """MD5 password manager.
 
@@ -155,6 +183,8 @@
     >>> encoded = manager.encodePassword(password, salt="")
     >>> encoded
     '{MD5}86dddccec45db4599f1ac00018e54139'
+    >>> manager.match(encoded)
+    True
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -163,6 +193,8 @@
     >>> encoded = manager.encodePassword(password)
     >>> encoded[-32:]
     '86dddccec45db4599f1ac00018e54139'
+    >>> manager.match(encoded)
+    True
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -181,6 +213,13 @@
 
     >>> manager.checkPassword(encoded, password)
     True
+
+    However, because the prefix is missing, the password manager cannot claim
+    to implement the scheme:
+
+    >>> manager.match(encoded)
+    False
+
     """
 
     implements(IPasswordManager)
@@ -197,7 +236,10 @@
         salt = encoded_password[:-32]
         return encoded_password == self.encodePassword(password, salt)[5:]
 
+    def match(self, encoded_password):
+        return encoded_password.startswith('{MD5}')
 
+
 class SHA1PasswordManager(PlainTextPasswordManager):
     """SHA1 password manager.
 
@@ -214,6 +256,8 @@
     >>> encoded = manager.encodePassword(password, salt="")
     >>> encoded
     '{SHA1}04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+    >>> manager.match(encoded)
+    True
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -222,6 +266,8 @@
     >>> encoded = manager.encodePassword(password)
     >>> encoded[-40:]
     '04b4eec7154c5f3a2ec6d2956fb80b80dc737402'
+    >>> manager.match(encoded)
+    True
     >>> manager.checkPassword(encoded, password)
     True
     >>> manager.checkPassword(encoded, password + u"wrong")
@@ -241,6 +287,12 @@
     >>> manager.checkPassword(encoded, password)
     True
 
+    However, because the prefix is missing, the password manager cannot claim
+    to implement the scheme:
+
+    >>> manager.match(encoded)
+    False
+
     """
 
     implements(IPasswordManager)
@@ -257,7 +309,10 @@
         salt = encoded_password[:-40]
         return encoded_password == self.encodePassword(password, salt)[6:]
 
+    def match(self, encoded_password):
+        return encoded_password.startswith('{SHA1}')
 
+
 # Simple registry
 managers = [
     ('Plain Text', PlainTextPasswordManager()),



More information about the checkins mailing list