Mercurial > p > roundup > code
changeset 5488:52cb53eedf77
reworked random number use
prefer secrets module from Python 3.6+, random.SystemRandom and finally plain random
| author | Christof Meerwald <cmeerw@cmeerw.org> |
|---|---|
| date | Sat, 04 Aug 2018 22:40:16 +0100 |
| parents | ce171c81d823 |
| children | 8a91503c44b2 |
| files | roundup/anypy/random_.py roundup/cgi/actions.py roundup/cgi/client.py roundup/cgi/templating.py roundup/mailgw.py roundup/password.py roundup/roundupdb.py test/db_test_base.py test/test_cgi.py test/test_templating.py |
| diffstat | 10 files changed, 93 insertions(+), 64 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/roundup/anypy/random_.py Sat Aug 04 22:40:16 2018 +0100 @@ -0,0 +1,45 @@ +try: + from secrets import choice, randbelow, token_bytes + def seed(v = None): + pass + + is_weak = False +except ImportError: + import os as _os + import random as _random + + # prefer to use SystemRandom if it is available + if hasattr(_random, 'SystemRandom'): + def seed(v = None): + pass + + _r = _random.SystemRandom() + is_weak = False + else: + # don't completely throw away the existing state, but add some + # more random state to the existing state + def seed(v = None): + import os, time + _r.seed((_r.getstate(), + v, + hasattr(os, 'getpid') and os.getpid(), + time.time())) + + # create our own instance so we don't mess with the global + # random number generator + _r = _random.Random() + seed() + is_weak = True + + choice = _r.choice + + def randbelow(i): + return _r.randint(0, i - 1) + + if hasattr(_os, 'urandom'): + def token_bytes(l): + return _os.urandom(l) + else: + def token_bytes(l): + _bchr = chr if str == bytes else lambda x: bytes((x,)) + return b''.join([_bchr(_r.getrandbits(8)) for i in range(l)])
--- a/roundup/cgi/actions.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/cgi/actions.py Sat Aug 04 22:40:16 2018 +0100 @@ -1,4 +1,4 @@ -import re, cgi, time, random, csv, codecs +import re, cgi, time, csv, codecs from roundup import hyperdb, token, date, password from roundup.actions import Action as BaseAction @@ -8,6 +8,7 @@ from roundup.exceptions import Reject, RejectRaw from roundup.anypy import urllib_ from roundup.anypy.strings import StringIO +import roundup.anypy.random_ as random_ # Also add action to client.py::Client.actions property __all__ = ['Action', 'ShowAction', 'RetireAction', 'RestoreAction', 'SearchAction', @@ -963,9 +964,9 @@ return # generate the one-time-key and store the props for later - otk = ''.join([random.choice(chars) for x in range(32)]) + otk = ''.join([random_.choice(chars) for x in range(32)]) while otks.exists(otk): - otk = ''.join([random.choice(chars) for x in range(32)]) + otk = ''.join([random_.choice(chars) for x in range(32)]) otks.set(otk, uid=uid, uaddress=address) otks.commit() @@ -1084,9 +1085,9 @@ elif isinstance(proptype, hyperdb.Password): user_props[propname] = str(value) otks = self.db.getOTKManager() - otk = ''.join([random.choice(chars) for x in range(32)]) + otk = ''.join([random_.choice(chars) for x in range(32)]) while otks.exists(otk): - otk = ''.join([random.choice(chars) for x in range(32)]) + otk = ''.join([random_.choice(chars) for x in range(32)]) otks.set(otk, **user_props) # send the email
--- a/roundup/cgi/client.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/cgi/client.py Sat Aug 04 22:40:16 2018 +0100 @@ -11,13 +11,10 @@ import email.utils from traceback import format_exc -try: - # Use the cryptographic source of randomness if available - from random import SystemRandom - random=SystemRandom() +import roundup.anypy.random_ as random_ +if not random_.is_weak: logger.debug("Importing good random generator") -except ImportError: - from random import random +else: logger.warning("**SystemRandom not available. Using poor random generator") try: @@ -177,8 +174,7 @@ def _gen_sid(self): """ generate a unique session key """ while 1: - s = '%s%s'%(time.time(), random.random()) - s = b2s(binascii.b2a_base64(s2b(s)).strip()) + s = b2s(binascii.b2a_base64(random_.token_bytes(32)).strip()) if not self.session_db.exists(s): break @@ -323,7 +319,7 @@ def __init__(self, instance, request, env, form=None, translator=None): # re-seed the random number generator. Is this is an instance of # random.SystemRandom it has no effect. - random.seed() + random_.seed() # So we also seed the pseudorandom random source obtained from # import random # to make sure that every forked copy of the client will return @@ -401,8 +397,7 @@ def _gen_nonce(self): """ generate a unique nonce """ - n = '%s%s%s'%(random.random(), id(self), time.time() ) - n = hashlib.sha256(s2b(n)).hexdigest() + n = b2s(base64.b32encode(random_.token_bytes(40))) return n def setTranslator(self, translator=None): @@ -864,7 +859,7 @@ # try to seed with something harder to guess than # just the time. If random is SystemRandom, # this is a no-op. - random.seed("%s%s"%(password,time.time())) + random_.seed("%s%s"%(password,time.time())) # if user was not set by http authorization, try session lookup if not user:
--- a/roundup/cgi/templating.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/cgi/templating.py Sat Aug 04 22:40:16 2018 +0100 @@ -20,7 +20,7 @@ __docformat__ = 'restructuredtext' -import cgi, re, os.path, mimetypes, csv, string +import base64, cgi, re, os.path, mimetypes, csv, string import calendar import textwrap import time, hashlib @@ -29,16 +29,11 @@ from roundup import hyperdb, date, support from roundup import i18n from roundup.i18n import _ -from roundup.anypy.strings import is_us, s2b, us2s, s2u, u2s, StringIO +from roundup.anypy.strings import is_us, b2s, s2b, us2s, s2u, u2s, StringIO from .KeywordsExpr import render_keywords_expression_editor -try: - # Use the cryptographic source of randomness if available - from random import SystemRandom - random=SystemRandom() -except ImportError: - from random import random +import roundup.anypy.random_ as random_ try: import cPickle as pickle except ImportError: @@ -68,27 +63,19 @@ # until all Web UI translations are done via client.translator object translationService = TranslationService.get_translation() -def anti_csrf_nonce(self, client, lifetime=None): +def anti_csrf_nonce(client, lifetime=None): ''' Create a nonce for defending against CSRF attack. - This creates a nonce by hex encoding the sha256 of - random.random(), the address of the object requesting - the nonce and time.time(). - Then it stores the nonce, the session id for the user and the user id in the one time key database for use by the csrf validator that runs in the client::inner_main module/function. ''' otks=client.db.getOTKManager() - # include id(self) as the exact location of self (including address) - # is unpredicatable (depends on number of previous connections etc.) - key = '%s%s%s'%(random.random(),id(self),time.time()) - key = hashlib.sha256(s2b(key)).hexdigest() + key = b2s(base64.b32encode(random_.token_bytes(40))) while otks.exists(key): - key = '%s%s%s'%(random.random(),id(self),time.time()) - key = hashlib.sha256(s2b(key)).hexdigest() + key = b2s(base64.b32encode(random_.token_bytes(40))) # lifetime is in minutes. if lifetime is None: @@ -784,7 +771,7 @@ return '' return self.input(type="hidden", name="@csrf", - value=anti_csrf_nonce(self, self._client)) + \ + value=anti_csrf_nonce(self._client)) + \ '\n' + \ self.input(type="hidden", name="@action", value=action) + \ '\n' + \ @@ -927,7 +914,7 @@ value=self.activity.local(0)) + \ '\n' + \ self.input(type="hidden", name="@csrf", - value=anti_csrf_nonce(self, self._client)) + \ + value=anti_csrf_nonce(self._client)) + \ '\n' + \ self.input(type="hidden", name="@action", value=action) + \ '\n' + \ @@ -3082,7 +3069,7 @@ overlap) def anti_csrf_nonce(self, lifetime=None): - return anti_csrf_nonce(self, self.client, lifetime=lifetime) + return anti_csrf_nonce(self.client, lifetime=lifetime) def url_quote(self, url): """URL-quote the supplied text."""
--- a/roundup/mailgw.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/mailgw.py Sat Aug 04 22:40:16 2018 +0100 @@ -95,8 +95,8 @@ from __future__ import print_function __docformat__ = 'restructuredtext' -import re, os, smtplib, socket, binascii, quopri -import time, random, sys, logging +import base64, re, os, smtplib, socket, binascii, quopri +import time, sys, logging import codecs import traceback import email.utils @@ -108,7 +108,8 @@ from roundup.mailer import Mailer, MessageSendError from roundup.i18n import _ from roundup.hyperdb import iter_roles -from roundup.anypy.strings import StringIO +from roundup.anypy.strings import b2s, StringIO +import roundup.anypy.random_ as random_ try: import pyme, pyme.core, pyme.constants, pyme.constants.sigsum @@ -1163,7 +1164,8 @@ messageid = self.message.getheader('message-id') # generate a messageid if there isn't one if not messageid: - messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), + messageid = "<%s.%s.%s%s@%s>"%(time.time(), + b2s(base64.b32encode(random_.token_bytes(10))), self.classname, self.nodeid, self.config['MAIL_DOMAIN']) if self.content is None:
--- a/roundup/password.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/password.py Sat Aug 04 22:40:16 2018 +0100 @@ -19,12 +19,13 @@ """ __docformat__ = 'restructuredtext' -import re, string, random +import re, string import os from base64 import b64encode, b64decode from hashlib import md5, sha1 from roundup.anypy.strings import us2s, b2s, s2b +import roundup.anypy.random_ as random_ try: import crypt @@ -50,9 +51,6 @@ # Python 3. Elements of bytes are integers. return c -def getrandbytes(count): - return _bjoin(bchr(random.randint(0,255)) for i in range(count)) - #NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size, # and have charset that's compatible w/ unix crypt variants def h64encode(data): @@ -167,7 +165,7 @@ if other: rounds, salt, raw_salt, digest = pbkdf2_unpack(other) else: - raw_salt = getrandbytes(20) + raw_salt = random_.token_bytes(20) salt = h64encode(raw_salt) if config: rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS @@ -184,8 +182,8 @@ else: #new password # variable salt length - salt_len = random.randrange(36, 52) - salt = os.urandom(salt_len) + salt_len = random_.randbelow(52-36) + 36 + salt = random_.token_bytes(salt_len) s = ssha(s2b(plaintext), salt) elif scheme == 'SHA': s = sha1(s2b(plaintext)).hexdigest() @@ -196,7 +194,7 @@ salt = other else: saltchars = './0123456789'+string.ascii_letters - salt = random.choice(saltchars) + random.choice(saltchars) + salt = random_.choice(saltchars) + random_.choice(saltchars) s = crypt.crypt(plaintext, salt) elif scheme == 'plaintext': s = plaintext @@ -206,10 +204,10 @@ def generatePassword(length=12): chars = string.ascii_letters+string.digits - password = [random.choice(chars) for x in range(length)] + password = [random_.choice(chars) for x in range(length - 1)] # make sure there is at least one digit - password[0] = random.choice(string.digits) - random.shuffle(password) + digitidx = random_.randbelow(length) + password[digitidx:digitidx] = [random_.choice(string.digits)] return ''.join(password) class JournalPassword:
--- a/roundup/roundupdb.py Sun Aug 05 11:45:43 2018 +0000 +++ b/roundup/roundupdb.py Sat Aug 04 22:40:16 2018 +0100 @@ -20,7 +20,7 @@ """ __docformat__ = 'restructuredtext' -import re, os, smtplib, socket, time, random +import re, os, smtplib, socket, time import base64, mimetypes import os.path import logging @@ -39,7 +39,8 @@ from roundup.mailer import Mailer, MessageSendError, encode_quopri, \ nice_sender_header -from roundup.anypy.strings import s2u +from roundup.anypy.strings import b2s, s2u +import roundup.anypy.random_ as random_ try: import pyme, pyme.core @@ -421,9 +422,9 @@ if not messageid: # this is an old message that didn't get a messageid, so # create one - messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(), - self.classname, issueid, - self.db.config.MAIL_DOMAIN) + messageid = "<%s.%s.%s%s@%s>"%(time.time(), + b2s(base64.b32encode(random_.token_bytes(10))), + self.classname, issueid, self.db.config['MAIL_DOMAIN']) if msgid is not None: messages.set(msgid, messageid=messageid)
--- a/test/db_test_base.py Sun Aug 05 11:45:43 2018 +0000 +++ b/test/db_test_base.py Sat Aug 04 22:40:16 2018 +0100 @@ -3370,7 +3370,7 @@ def testInnerMain(self): cl = self.client cl.session_api = MockNull(_sid="1234567890") - self.form ['@nonce'] = anti_csrf_nonce(cl, cl) + self.form ['@nonce'] = anti_csrf_nonce(cl) cl.form = makeForm(self.form) # inner_main will re-open the database! # Note that in the template above, the rendering of the
--- a/test/test_cgi.py Sun Aug 05 11:45:43 2018 +0000 +++ b/test/test_cgi.py Sat Aug 04 22:40:16 2018 +0100 @@ -953,7 +953,7 @@ del(out[0]) form2 = copy.copy(form) - nonce = anti_csrf_nonce(cl, cl) + nonce = anti_csrf_nonce(cl) # verify that we can see the nonce otks = cl.db.getOTKManager() isitthere = otks.exists(nonce) @@ -985,7 +985,7 @@ cl.env['REQUEST_METHOD'] = 'GET' cl.env['HTTP_REFERER'] = 'http://whoami.com/path/' form2 = copy.copy(form) - nonce = anti_csrf_nonce(cl, cl) + nonce = anti_csrf_nonce(cl) form2.update({'@csrf': nonce}) # add a real csrf field to the form and rerun the inner_main cl.form = db_test_base.makeForm(form2)
--- a/test/test_templating.py Sun Aug 05 11:45:43 2018 +0000 +++ b/test/test_templating.py Sat Aug 04 22:40:16 2018 +0100 @@ -153,7 +153,7 @@ if test == 'module': # test the module function - nonce1 = anti_csrf_nonce(self, self.client, lifetime=1) + nonce1 = anti_csrf_nonce(self.client, lifetime=1) # lifetime * 60 is the offset greater_than = week_seconds - 1 * 60 elif test == 'template': @@ -163,7 +163,7 @@ greater_than = week_seconds - 5 * 60 elif test == 'default_time': # use the module function but with no lifetime - nonce1 = anti_csrf_nonce(self, self.client) + nonce1 = anti_csrf_nonce(self.client) # see above for web nonce lifetime. greater_than = week_seconds - 10 * 60
