Commit 7f46f181 authored by Hanno Schlichting's avatar Hanno Schlichting

LP #1071067: Use a stronger random number generator and a constant time comparison function.

parent 485c4b74
...@@ -5,9 +5,12 @@ This file contains change information for the current Zope release. ...@@ -5,9 +5,12 @@ This file contains change information for the current Zope release.
Change information for previous versions of Zope can be found at Change information for previous versions of Zope can be found at
http://docs.zope.org/zope2/releases/. http://docs.zope.org/zope2/releases/.
2.12.26 (unreleased) 2.12.26 (2012-10-31)
-------------------- --------------------
- LP #1071067: Use a stronger random number generator and a constant time
comparison function.
- LP #930812: Scrub headers a bit more. - LP #930812: Scrub headers a bit more.
2.12.25 (2012-09-18) 2.12.25 (2012-09-18)
......
...@@ -16,7 +16,7 @@ import os ...@@ -16,7 +16,7 @@ import os
from setuptools import setup, find_packages, Extension from setuptools import setup, find_packages, Extension
setup(name='Zope2', setup(name='Zope2',
version='2.12.26dev', version='2.12.26',
url='http://www.zope.org', url='http://www.zope.org',
license='ZPL 2.1', license='ZPL 2.1',
description='Zope2 application server / web framework', description='Zope2 application server / web framework',
......
...@@ -11,16 +11,57 @@ ...@@ -11,16 +11,57 @@
# #
############################################################################## ##############################################################################
__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: try:
from hashlib import sha1 as sha random = random.SystemRandom()
except: using_sysrandom = True
from sha import new as sha except NotImplementedError:
using_sysrandom = False
import binascii
from binascii import b2a_base64, a2b_base64 def _reseed():
from random import choice, randrange 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 class PasswordEncryptionScheme: # An Interface
...@@ -40,12 +81,14 @@ class PasswordEncryptionScheme: # An Interface ...@@ -40,12 +81,14 @@ class PasswordEncryptionScheme: # An Interface
_schemes = [] _schemes = []
def registerScheme(id, s): def registerScheme(id, s):
''' '''
Registers an LDAP password encoding scheme. Registers an LDAP password encoding scheme.
''' '''
_schemes.append((id, '{%s}' % id, s)) _schemes.append((id, '{%s}' % id, s))
def listSchemes(): def listSchemes():
r = [] r = []
for id, prefix, scheme in _schemes: for id, prefix, scheme in _schemes:
...@@ -67,7 +110,7 @@ class SSHADigestScheme: ...@@ -67,7 +110,7 @@ class SSHADigestScheme:
# All 256 characters are available. # All 256 characters are available.
salt = '' salt = ''
for n in range(7): for n in range(7):
salt += chr(randrange(256)) salt += chr(_randrange(256))
return salt return salt
def encrypt(self, pw): def encrypt(self, pw):
...@@ -83,7 +126,7 @@ class SSHADigestScheme: ...@@ -83,7 +126,7 @@ class SSHADigestScheme:
return 0 return 0
salt = ref[20:] salt = ref[20:]
compare = b2a_base64(sha(attempt + salt).digest() + salt)[:-1] compare = b2a_base64(sha(attempt + salt).digest() + salt)[:-1]
return (compare == reference) return constant_time_compare(compare, reference)
registerScheme('SSHA', SSHADigestScheme()) registerScheme('SSHA', SSHADigestScheme())
...@@ -95,7 +138,7 @@ class SHADigestScheme: ...@@ -95,7 +138,7 @@ class SHADigestScheme:
def validate(self, reference, attempt): def validate(self, reference, attempt):
compare = b2a_base64(sha(attempt).digest())[:-1] compare = b2a_base64(sha(attempt).digest())[:-1]
return (compare == reference) return constant_time_compare(compare, reference)
registerScheme('SHA', SHADigestScheme()) registerScheme('SHA', SHADigestScheme())
...@@ -114,14 +157,14 @@ if crypt is not None: ...@@ -114,14 +157,14 @@ if crypt is not None:
choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz"
"0123456789./") "0123456789./")
return choice(choices) + choice(choices) return _choice(choices) + _choice(choices)
def encrypt(self, pw): def encrypt(self, pw):
return crypt(pw, self.generate_salt()) return crypt(pw, self.generate_salt())
def validate(self, reference, attempt): def validate(self, reference, attempt):
a = crypt(attempt, reference[:2]) a = crypt(attempt, reference[:2])
return (a == reference) return constant_time_compare(a, reference)
registerScheme('CRYPT', CryptDigestScheme()) registerScheme('CRYPT', CryptDigestScheme())
...@@ -144,7 +187,7 @@ class MySQLDigestScheme: ...@@ -144,7 +187,7 @@ class MySQLDigestScheme:
def validate(self, reference, attempt): def validate(self, reference, attempt):
a = self.encrypt(attempt) a = self.encrypt(attempt)
return (a == reference) return constant_time_compare(a, reference)
registerScheme('MYSQL', MySQLDigestScheme()) registerScheme('MYSQL', MySQLDigestScheme())
...@@ -158,7 +201,8 @@ def pw_validate(reference, attempt): ...@@ -158,7 +201,8 @@ def pw_validate(reference, attempt):
if reference[:lp] == prefix: if reference[:lp] == prefix:
return scheme.validate(reference[lp:], attempt) return scheme.validate(reference[lp:], attempt)
# Assume cleartext. # Assume cleartext.
return (reference == attempt) return constant_time_compare(reference, attempt)
def is_encrypted(pw): def is_encrypted(pw):
for id, prefix, scheme in _schemes: for id, prefix, scheme in _schemes:
...@@ -167,12 +211,13 @@ def is_encrypted(pw): ...@@ -167,12 +211,13 @@ def is_encrypted(pw):
return 1 return 1
return 0 return 0
def pw_encrypt(pw, encoding='SSHA'): def pw_encrypt(pw, encoding='SSHA'):
"""Encrypt the provided plain text password using the encoding if provided """Encrypt the provided plain text password using the encoding if provided
and return it in an LDAP-style representation.""" and return it in an LDAP-style representation."""
for id, prefix, scheme in _schemes: for id, prefix, scheme in _schemes:
if encoding == id: if encoding == id:
return prefix + scheme.encrypt(pw) return prefix + scheme.encrypt(pw)
raise ValueError, 'Not supported: %s' % encoding raise ValueError('Not supported: %s' % encoding)
pw_encode = pw_encrypt # backward compatibility pw_encode = pw_encrypt # backward compatibility
...@@ -13,8 +13,9 @@ ...@@ -13,8 +13,9 @@
import binascii import binascii
from cgi import escape from cgi import escape
from hashlib import sha256
import logging import logging
import random import os
import re import re
import string import string
import sys import sys
...@@ -63,6 +64,29 @@ TRAVERSAL_APPHANDLE = 'BrowserIdManager' ...@@ -63,6 +64,29 @@ TRAVERSAL_APPHANDLE = 'BrowserIdManager'
LOG = logging.getLogger('Zope.BrowserIdManager') LOG = logging.getLogger('Zope.BrowserIdManager')
# Use the system PRNG if possible
import random
try:
random = random.SystemRandom()
using_sysrandom = True
except NotImplementedError:
using_sysrandom = False
def _randint(start, end):
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(), os.getpid())
).digest())
return random.randint(start, end)
def constructBrowserIdManager( def constructBrowserIdManager(
self, id=BROWSERID_MANAGER_NAME, title='', idname='_ZopeId', self, id=BROWSERID_MANAGER_NAME, title='', idname='_ZopeId',
location=('cookies', 'form'), cookiepath='/', cookiedomain='', location=('cookies', 'form'), cookiepath='/', cookiedomain='',
...@@ -558,7 +582,7 @@ def isAWellFormedBrowserId(bid, binerr=binascii.Error): ...@@ -558,7 +582,7 @@ def isAWellFormedBrowserId(bid, binerr=binascii.Error):
return None return None
def getNewBrowserId(randint=random.randint, maxint=99999999): def getNewBrowserId(randint=_randint, maxint=99999999):
""" Returns 19-character string browser id """ Returns 19-character string browser id
'AAAAAAAABBBBBBBB' 'AAAAAAAABBBBBBBB'
where: where:
...@@ -573,5 +597,4 @@ def getNewBrowserId(randint=random.randint, maxint=99999999): ...@@ -573,5 +597,4 @@ def getNewBrowserId(randint=random.randint, maxint=99999999):
An example is: 89972317A0C3EHnUi90w An example is: 89972317A0C3EHnUi90w
""" """
return '%08i%s' % (randint(0, maxint-1), getB64TStamp()) return '%08i%s' % (randint(0, maxint - 1), getB64TStamp())
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
versions = versions versions = versions
[versions] [versions]
Zope2 = Zope2 = 2.12.26
Acquisition = 2.13.8 Acquisition = 2.13.8
buildout.dumppickedversions = 0.4 buildout.dumppickedversions = 0.4
ClientForm = 0.2.10 ClientForm = 0.2.10
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment