[Checkins] SVN: AccessControl/branches/2.13/ LP #1071067: Use a stronger random number generator and a constant time comparison function.

Hano Schlichting cvs-admin at zope.org
Wed Oct 31 14:08:29 UTC 2012


Log message for revision 128152:
  LP #1071067: Use a stronger random number generator and a constant time comparison function.
  

Changed:
  U   AccessControl/branches/2.13/CHANGES.txt
  U   AccessControl/branches/2.13/setup.py
  U   AccessControl/branches/2.13/src/AccessControl/AuthEncoding.py

-=-
Modified: AccessControl/branches/2.13/CHANGES.txt
===================================================================
--- AccessControl/branches/2.13/CHANGES.txt	2012-10-30 15:42:05 UTC (rev 128151)
+++ AccessControl/branches/2.13/CHANGES.txt	2012-10-31 14:08:28 UTC (rev 128152)
@@ -1,9 +1,11 @@
 Changelog
 =========
 
-2.13.12 (unreleased)
+2.13.12 (2012-10-31)
 --------------------
 
+- LP #1071067: Use a stronger random number generator and a constant time
+  comparison function.
 
 2.13.11 (2012-10-21)
 --------------------

Modified: AccessControl/branches/2.13/setup.py
===================================================================
--- AccessControl/branches/2.13/setup.py	2012-10-30 15:42:05 UTC (rev 128151)
+++ AccessControl/branches/2.13/setup.py	2012-10-31 14:08:28 UTC (rev 128152)
@@ -16,7 +16,7 @@
 from setuptools import setup, find_packages, Extension
 
 setup(name='AccessControl',
-      version = '2.13.12dev',
+      version = '2.13.12',
       url='http://pypi.python.org/pypi/AccessControl',
       license='ZPL 2.1',
       description="Security framework for Zope2.",

Modified: AccessControl/branches/2.13/src/AccessControl/AuthEncoding.py
===================================================================
--- AccessControl/branches/2.13/src/AccessControl/AuthEncoding.py	2012-10-30 15:42:05 UTC (rev 128151)
+++ AccessControl/branches/2.13/src/AccessControl/AuthEncoding.py	2012-10-31 14:08:28 UTC (rev 128152)
@@ -11,18 +11,59 @@
 #
 ##############################################################################
 
-__version__='$Revision: 1.9 $'[11:-2]
+import binascii
+from binascii import b2a_base64, a2b_base64
+from hashlib import sha1 as sha
+from hashlib import sha256
+from os import getpid
+import time
 
+# Use the system PRNG if possible
+import random
 try:
-    from hashlib import sha1 as sha
-except:
-    from sha import new as sha
+    random = random.SystemRandom()
+    using_sysrandom = True
+except NotImplementedError:
+    using_sysrandom = False
 
-import binascii
-from binascii import b2a_base64, a2b_base64
-from random import choice, randrange
 
+def _reseed():
+    if not using_sysrandom:
+        # This is ugly, and a hack, but it makes things better than
+        # the alternative of predictability. This re-seeds the PRNG
+        # using a value that is hard for an attacker to predict, every
+        # time a random string is required. This may change the
+        # properties of the chosen random sequence slightly, but this
+        # is better than absolute predictability.
+        random.seed(sha256(
+            "%s%s%s" % (random.getstate(), time.time(), getpid())
+        ).digest())
 
+
+def _choice(c):
+    _reseed()
+    return random.choice(c)
+
+
+def _randrange(r):
+    _reseed()
+    return random.randrange(r)
+
+
+def constant_time_compare(val1, val2):
+    """
+    Returns True if the two strings are equal, False otherwise.
+
+    The time taken is independent of the number of characters that match.
+    """
+    if len(val1) != len(val2):
+        return False
+    result = 0
+    for x, y in zip(val1, val2):
+        result |= ord(x) ^ ord(y)
+    return result == 0
+
+
 class PasswordEncryptionScheme:  # An Interface
 
     def encrypt(pw):
@@ -40,12 +81,14 @@
 
 _schemes = []
 
+
 def registerScheme(id, s):
     '''
     Registers an LDAP password encoding scheme.
     '''
     _schemes.append((id, '{%s}' % id, s))
 
+
 def listSchemes():
     r = []
     for id, prefix, scheme in _schemes:
@@ -67,7 +110,7 @@
         # All 256 characters are available.
         salt = ''
         for n in range(7):
-            salt += chr(randrange(256))
+            salt += chr(_randrange(256))
         return salt
 
     def encrypt(self, pw):
@@ -83,7 +126,7 @@
             return 0
         salt = ref[20:]
         compare = b2a_base64(sha(attempt + salt).digest() + salt)[:-1]
-        return (compare == reference)
+        return constant_time_compare(compare, reference)
 
 registerScheme('SSHA', SSHADigestScheme())
 
@@ -95,7 +138,7 @@
 
     def validate(self, reference, attempt):
         compare = b2a_base64(sha(attempt).digest())[:-1]
-        return (compare == reference)
+        return constant_time_compare(compare, reference)
 
 registerScheme('SHA', SHADigestScheme())
 
@@ -114,14 +157,14 @@
             choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                        "abcdefghijklmnopqrstuvwxyz"
                        "0123456789./")
-            return choice(choices) + choice(choices)
+            return _choice(choices) + _choice(choices)
 
         def encrypt(self, pw):
             return crypt(pw, self.generate_salt())
 
         def validate(self, reference, attempt):
             a = crypt(attempt, reference[:2])
-            return (a == reference)
+            return constant_time_compare(a, reference)
 
     registerScheme('CRYPT', CryptDigestScheme())
 
@@ -144,7 +187,7 @@
 
     def validate(self, reference, attempt):
         a = self.encrypt(attempt)
-        return (a == reference)
+        return constant_time_compare(a, reference)
 
 registerScheme('MYSQL', MySQLDigestScheme())
 
@@ -158,8 +201,9 @@
         if reference[:lp] == prefix:
             return scheme.validate(reference[lp:], attempt)
     # Assume cleartext.
-    return (reference == attempt)
+    return constant_time_compare(reference, attempt)
 
+
 def is_encrypted(pw):
     for id, prefix, scheme in _schemes:
         lp = len(prefix)
@@ -167,12 +211,13 @@
             return 1
     return 0
 
+
 def pw_encrypt(pw, encoding='SSHA'):
     """Encrypt the provided plain text password using the encoding if provided
     and return it in an LDAP-style representation."""
     for id, prefix, scheme in _schemes:
         if encoding == id:
             return prefix + scheme.encrypt(pw)
-    raise ValueError, 'Not supported: %s' % encoding
+    raise ValueError('Not supported: %s' % encoding)
 
 pw_encode = pw_encrypt  # backward compatibility



More information about the checkins mailing list