Skip to content

Commit 69f9f63

Browse files
committed
reworked random number use
prefer secrets module from Python 3.6+, random.SystemRandom and finally plain random
1 parent ae4edd6 commit 69f9f63

File tree

10 files changed

+93
-64
lines changed

10 files changed

+93
-64
lines changed

roundup/anypy/random_.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
try:
2+
from secrets import choice, randbelow, token_bytes
3+
def seed(v = None):
4+
pass
5+
6+
is_weak = False
7+
except ImportError:
8+
import os as _os
9+
import random as _random
10+
11+
# prefer to use SystemRandom if it is available
12+
if hasattr(_random, 'SystemRandom'):
13+
def seed(v = None):
14+
pass
15+
16+
_r = _random.SystemRandom()
17+
is_weak = False
18+
else:
19+
# don't completely throw away the existing state, but add some
20+
# more random state to the existing state
21+
def seed(v = None):
22+
import os, time
23+
_r.seed((_r.getstate(),
24+
v,
25+
hasattr(os, 'getpid') and os.getpid(),
26+
time.time()))
27+
28+
# create our own instance so we don't mess with the global
29+
# random number generator
30+
_r = _random.Random()
31+
seed()
32+
is_weak = True
33+
34+
choice = _r.choice
35+
36+
def randbelow(i):
37+
return _r.randint(0, i - 1)
38+
39+
if hasattr(_os, 'urandom'):
40+
def token_bytes(l):
41+
return _os.urandom(l)
42+
else:
43+
def token_bytes(l):
44+
_bchr = chr if str == bytes else lambda x: bytes((x,))
45+
return b''.join([_bchr(_r.getrandbits(8)) for i in range(l)])

roundup/cgi/actions.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import re, cgi, time, random, csv, codecs
1+
import re, cgi, time, csv, codecs
22

33
from roundup import hyperdb, token, date, password
44
from roundup.actions import Action as BaseAction
@@ -8,6 +8,7 @@
88
from roundup.exceptions import Reject, RejectRaw
99
from roundup.anypy import urllib_
1010
from roundup.anypy.strings import StringIO
11+
import roundup.anypy.random_ as random_
1112

1213
# Also add action to client.py::Client.actions property
1314
__all__ = ['Action', 'ShowAction', 'RetireAction', 'RestoreAction', 'SearchAction',
@@ -963,9 +964,9 @@ def handle(self):
963964
return
964965

965966
# generate the one-time-key and store the props for later
966-
otk = ''.join([random.choice(chars) for x in range(32)])
967+
otk = ''.join([random_.choice(chars) for x in range(32)])
967968
while otks.exists(otk):
968-
otk = ''.join([random.choice(chars) for x in range(32)])
969+
otk = ''.join([random_.choice(chars) for x in range(32)])
969970
otks.set(otk, uid=uid, uaddress=address)
970971
otks.commit()
971972

@@ -1084,9 +1085,9 @@ def handle(self):
10841085
elif isinstance(proptype, hyperdb.Password):
10851086
user_props[propname] = str(value)
10861087
otks = self.db.getOTKManager()
1087-
otk = ''.join([random.choice(chars) for x in range(32)])
1088+
otk = ''.join([random_.choice(chars) for x in range(32)])
10881089
while otks.exists(otk):
1089-
otk = ''.join([random.choice(chars) for x in range(32)])
1090+
otk = ''.join([random_.choice(chars) for x in range(32)])
10901091
otks.set(otk, **user_props)
10911092

10921093
# send the email

roundup/cgi/client.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111
import email.utils
1212
from traceback import format_exc
1313

14-
try:
15-
# Use the cryptographic source of randomness if available
16-
from random import SystemRandom
17-
random=SystemRandom()
14+
import roundup.anypy.random_ as random_
15+
if not random_.is_weak:
1816
logger.debug("Importing good random generator")
19-
except ImportError:
20-
from random import random
17+
else:
2118
logger.warning("**SystemRandom not available. Using poor random generator")
2219

2320
try:
@@ -177,8 +174,7 @@ def __init__(self, client):
177174
def _gen_sid(self):
178175
""" generate a unique session key """
179176
while 1:
180-
s = '%s%s'%(time.time(), random.random())
181-
s = b2s(binascii.b2a_base64(s2b(s)).strip())
177+
s = b2s(binascii.b2a_base64(random_.token_bytes(32)).strip())
182178
if not self.session_db.exists(s):
183179
break
184180

@@ -323,7 +319,7 @@ class Client:
323319
def __init__(self, instance, request, env, form=None, translator=None):
324320
# re-seed the random number generator. Is this is an instance of
325321
# random.SystemRandom it has no effect.
326-
random.seed()
322+
random_.seed()
327323
# So we also seed the pseudorandom random source obtained from
328324
# import random
329325
# to make sure that every forked copy of the client will return
@@ -401,8 +397,7 @@ def __init__(self, instance, request, env, form=None, translator=None):
401397

402398
def _gen_nonce(self):
403399
""" generate a unique nonce """
404-
n = '%s%s%s'%(random.random(), id(self), time.time() )
405-
n = hashlib.sha256(s2b(n)).hexdigest()
400+
n = b2s(base64.b32encode(random_.token_bytes(40)))
406401
return n
407402

408403
def setTranslator(self, translator=None):
@@ -864,7 +859,7 @@ def determine_user(self):
864859
# try to seed with something harder to guess than
865860
# just the time. If random is SystemRandom,
866861
# this is a no-op.
867-
random.seed("%s%s"%(password,time.time()))
862+
random_.seed("%s%s"%(password,time.time()))
868863

869864
# if user was not set by http authorization, try session lookup
870865
if not user:

roundup/cgi/templating.py

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
__docformat__ = 'restructuredtext'
2121

2222

23-
import cgi, re, os.path, mimetypes, csv, string
23+
import base64, cgi, re, os.path, mimetypes, csv, string
2424
import calendar
2525
import textwrap
2626
import time, hashlib
@@ -29,16 +29,11 @@
2929
from roundup import hyperdb, date, support
3030
from roundup import i18n
3131
from roundup.i18n import _
32-
from roundup.anypy.strings import is_us, s2b, us2s, s2u, u2s, StringIO
32+
from roundup.anypy.strings import is_us, b2s, s2b, us2s, s2u, u2s, StringIO
3333

3434
from .KeywordsExpr import render_keywords_expression_editor
3535

36-
try:
37-
# Use the cryptographic source of randomness if available
38-
from random import SystemRandom
39-
random=SystemRandom()
40-
except ImportError:
41-
from random import random
36+
import roundup.anypy.random_ as random_
4237
try:
4338
import cPickle as pickle
4439
except ImportError:
@@ -68,27 +63,19 @@
6863
# until all Web UI translations are done via client.translator object
6964
translationService = TranslationService.get_translation()
7065

71-
def anti_csrf_nonce(self, client, lifetime=None):
66+
def anti_csrf_nonce(client, lifetime=None):
7267
''' Create a nonce for defending against CSRF attack.
7368
74-
This creates a nonce by hex encoding the sha256 of
75-
random.random(), the address of the object requesting
76-
the nonce and time.time().
77-
7869
Then it stores the nonce, the session id for the user
7970
and the user id in the one time key database for use
8071
by the csrf validator that runs in the client::inner_main
8172
module/function.
8273
'''
8374
otks=client.db.getOTKManager()
84-
# include id(self) as the exact location of self (including address)
85-
# is unpredicatable (depends on number of previous connections etc.)
86-
key = '%s%s%s'%(random.random(),id(self),time.time())
87-
key = hashlib.sha256(s2b(key)).hexdigest()
75+
key = b2s(base64.b32encode(random_.token_bytes(40)))
8876

8977
while otks.exists(key):
90-
key = '%s%s%s'%(random.random(),id(self),time.time())
91-
key = hashlib.sha256(s2b(key)).hexdigest()
78+
key = b2s(base64.b32encode(random_.token_bytes(40)))
9279

9380
# lifetime is in minutes.
9481
if lifetime is None:
@@ -784,7 +771,7 @@ def submit(self, label=''"Submit New Entry", action="new"):
784771
return ''
785772

786773
return self.input(type="hidden", name="@csrf",
787-
value=anti_csrf_nonce(self, self._client)) + \
774+
value=anti_csrf_nonce(self._client)) + \
788775
'\n' + \
789776
self.input(type="hidden", name="@action", value=action) + \
790777
'\n' + \
@@ -927,7 +914,7 @@ def submit(self, label=''"Submit Changes", action="edit"):
927914
value=self.activity.local(0)) + \
928915
'\n' + \
929916
self.input(type="hidden", name="@csrf",
930-
value=anti_csrf_nonce(self, self._client)) + \
917+
value=anti_csrf_nonce(self._client)) + \
931918
'\n' + \
932919
self.input(type="hidden", name="@action", value=action) + \
933920
'\n' + \
@@ -3082,7 +3069,7 @@ def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
30823069
overlap)
30833070

30843071
def anti_csrf_nonce(self, lifetime=None):
3085-
return anti_csrf_nonce(self, self.client, lifetime=lifetime)
3072+
return anti_csrf_nonce(self.client, lifetime=lifetime)
30863073

30873074
def url_quote(self, url):
30883075
"""URL-quote the supplied text."""

roundup/mailgw.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ class node. Any parts of other types are each stored in separate files
9595
from __future__ import print_function
9696
__docformat__ = 'restructuredtext'
9797

98-
import re, os, smtplib, socket, binascii, quopri
99-
import time, random, sys, logging
98+
import base64, re, os, smtplib, socket, binascii, quopri
99+
import time, sys, logging
100100
import codecs
101101
import traceback
102102
import email.utils
@@ -108,7 +108,8 @@ class node. Any parts of other types are each stored in separate files
108108
from roundup.mailer import Mailer, MessageSendError
109109
from roundup.i18n import _
110110
from roundup.hyperdb import iter_roles
111-
from roundup.anypy.strings import StringIO
111+
from roundup.anypy.strings import b2s, StringIO
112+
import roundup.anypy.random_ as random_
112113

113114
try:
114115
import pyme, pyme.core, pyme.constants, pyme.constants.sigsum
@@ -1163,7 +1164,8 @@ def create_msg(self):
11631164
messageid = self.message.getheader('message-id')
11641165
# generate a messageid if there isn't one
11651166
if not messageid:
1166-
messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
1167+
messageid = "<%s.%s.%s%s@%s>"%(time.time(),
1168+
b2s(base64.b32encode(random_.token_bytes(10))),
11671169
self.classname, self.nodeid, self.config['MAIL_DOMAIN'])
11681170

11691171
if self.content is None:

roundup/password.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
"""
2020
__docformat__ = 'restructuredtext'
2121

22-
import re, string, random
22+
import re, string
2323
import os
2424
from base64 import b64encode, b64decode
2525
from hashlib import md5, sha1
2626

2727
from roundup.anypy.strings import us2s, b2s, s2b
28+
import roundup.anypy.random_ as random_
2829

2930
try:
3031
import crypt
@@ -50,9 +51,6 @@ def bord(c):
5051
# Python 3. Elements of bytes are integers.
5152
return c
5253

53-
def getrandbytes(count):
54-
return _bjoin(bchr(random.randint(0,255)) for i in range(count))
55-
5654
#NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size,
5755
# and have charset that's compatible w/ unix crypt variants
5856
def h64encode(data):
@@ -167,7 +165,7 @@ def encodePassword(plaintext, scheme, other=None, config=None):
167165
if other:
168166
rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
169167
else:
170-
raw_salt = getrandbytes(20)
168+
raw_salt = random_.token_bytes(20)
171169
salt = h64encode(raw_salt)
172170
if config:
173171
rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
@@ -184,8 +182,8 @@ def encodePassword(plaintext, scheme, other=None, config=None):
184182
else:
185183
#new password
186184
# variable salt length
187-
salt_len = random.randrange(36, 52)
188-
salt = os.urandom(salt_len)
185+
salt_len = random_.randbelow(52-36) + 36
186+
salt = random_.token_bytes(salt_len)
189187
s = ssha(s2b(plaintext), salt)
190188
elif scheme == 'SHA':
191189
s = sha1(s2b(plaintext)).hexdigest()
@@ -196,7 +194,7 @@ def encodePassword(plaintext, scheme, other=None, config=None):
196194
salt = other
197195
else:
198196
saltchars = './0123456789'+string.ascii_letters
199-
salt = random.choice(saltchars) + random.choice(saltchars)
197+
salt = random_.choice(saltchars) + random_.choice(saltchars)
200198
s = crypt.crypt(plaintext, salt)
201199
elif scheme == 'plaintext':
202200
s = plaintext
@@ -206,10 +204,10 @@ def encodePassword(plaintext, scheme, other=None, config=None):
206204

207205
def generatePassword(length=12):
208206
chars = string.ascii_letters+string.digits
209-
password = [random.choice(chars) for x in range(length)]
207+
password = [random_.choice(chars) for x in range(length - 1)]
210208
# make sure there is at least one digit
211-
password[0] = random.choice(string.digits)
212-
random.shuffle(password)
209+
digitidx = random_.randbelow(length)
210+
password[digitidx:digitidx] = [random_.choice(string.digits)]
213211
return ''.join(password)
214212

215213
class JournalPassword:

roundup/roundupdb.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"""
2121
__docformat__ = 'restructuredtext'
2222

23-
import re, os, smtplib, socket, time, random
23+
import re, os, smtplib, socket, time
2424
import base64, mimetypes
2525
import os.path
2626
import logging
@@ -39,7 +39,8 @@
3939
from roundup.mailer import Mailer, MessageSendError, encode_quopri, \
4040
nice_sender_header
4141

42-
from roundup.anypy.strings import s2u
42+
from roundup.anypy.strings import b2s, s2u
43+
import roundup.anypy.random_ as random_
4344

4445
try:
4546
import pyme, pyme.core
@@ -421,9 +422,9 @@ def send_message(self, issueid, msgid, note, sendto, from_address=None,
421422
if not messageid:
422423
# this is an old message that didn't get a messageid, so
423424
# create one
424-
messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
425-
self.classname, issueid,
426-
self.db.config.MAIL_DOMAIN)
425+
messageid = "<%s.%s.%s%s@%s>"%(time.time(),
426+
b2s(base64.b32encode(random_.token_bytes(10))),
427+
self.classname, issueid, self.db.config['MAIL_DOMAIN'])
427428
if msgid is not None:
428429
messages.set(msgid, messageid=messageid)
429430

test/db_test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3370,7 +3370,7 @@ def tearDown(self):
33703370
def testInnerMain(self):
33713371
cl = self.client
33723372
cl.session_api = MockNull(_sid="1234567890")
3373-
self.form ['@nonce'] = anti_csrf_nonce(cl, cl)
3373+
self.form ['@nonce'] = anti_csrf_nonce(cl)
33743374
cl.form = makeForm(self.form)
33753375
# inner_main will re-open the database!
33763376
# Note that in the template above, the rendering of the

test/test_cgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ def hasPermission(s, p, classname=None, d=None, e=None, **kw):
953953
del(out[0])
954954

955955
form2 = copy.copy(form)
956-
nonce = anti_csrf_nonce(cl, cl)
956+
nonce = anti_csrf_nonce(cl)
957957
# verify that we can see the nonce
958958
otks = cl.db.getOTKManager()
959959
isitthere = otks.exists(nonce)
@@ -985,7 +985,7 @@ def hasPermission(s, p, classname=None, d=None, e=None, **kw):
985985
cl.env['REQUEST_METHOD'] = 'GET'
986986
cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
987987
form2 = copy.copy(form)
988-
nonce = anti_csrf_nonce(cl, cl)
988+
nonce = anti_csrf_nonce(cl)
989989
form2.update({'@csrf': nonce})
990990
# add a real csrf field to the form and rerun the inner_main
991991
cl.form = db_test_base.makeForm(form2)

0 commit comments

Comments
 (0)