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.
Change information for previous versions of Zope can be found at
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.
2.12.25 (2012-09-18)
......
......@@ -16,7 +16,7 @@ import os
from setuptools import setup, find_packages, Extension
setup(name='Zope2',
version='2.12.26dev',
version='2.12.26',
url='http://www.zope.org',
license='ZPL 2.1',
description='Zope2 application server / web framework',
......
......@@ -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:
from hashlib import sha1 as sha
except:
from sha import new as sha
random = random.SystemRandom()
using_sysrandom = True
except NotImplementedError:
using_sysrandom = False
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)
import binascii
from binascii import b2a_base64, a2b_base64
from random import choice, randrange
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
......@@ -40,12 +81,14 @@ class PasswordEncryptionScheme: # An Interface
_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 @@ class SSHADigestScheme:
# 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 @@ class SSHADigestScheme:
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 @@ class SHADigestScheme:
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 @@ if crypt is not None:
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 @@ class MySQLDigestScheme:
def validate(self, reference, attempt):
a = self.encrypt(attempt)
return (a == reference)
return constant_time_compare(a, reference)
registerScheme('MYSQL', MySQLDigestScheme())
......@@ -158,7 +201,8 @@ def pw_validate(reference, attempt):
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:
......@@ -167,12 +211,13 @@ def is_encrypted(pw):
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
############################################################################
#
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
......@@ -13,8 +13,9 @@
import binascii
from cgi import escape
from hashlib import sha256
import logging
import random
import os
import re
import string
import sys
......@@ -63,6 +64,29 @@ TRAVERSAL_APPHANDLE = '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(
self, id=BROWSERID_MANAGER_NAME, title='', idname='_ZopeId',
location=('cookies', 'form'), cookiepath='/', cookiedomain='',
......@@ -558,7 +582,7 @@ def isAWellFormedBrowserId(bid, binerr=binascii.Error):
return None
def getNewBrowserId(randint=random.randint, maxint=99999999):
def getNewBrowserId(randint=_randint, maxint=99999999):
""" Returns 19-character string browser id
'AAAAAAAABBBBBBBB'
where:
......@@ -573,5 +597,4 @@ def getNewBrowserId(randint=random.randint, maxint=99999999):
An example is: 89972317A0C3EHnUi90w
"""
return '%08i%s' % (randint(0, maxint-1), getB64TStamp())
return '%08i%s' % (randint(0, maxint - 1), getB64TStamp())
......@@ -2,7 +2,7 @@
versions = versions
[versions]
Zope2 =
Zope2 = 2.12.26
Acquisition = 2.13.8
buildout.dumppickedversions = 0.4
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