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
 

Roundup Issue Tracker: http://roundup-tracker.org/