Skip to content

Commit 9f5fe79

Browse files
authored
bpo-40286: Add randbytes() method to random.Random (GH-19527)
Add random.randbytes() function and random.Random.randbytes() method to generate random bytes. Modify secrets.token_bytes() to use SystemRandom.randbytes() rather than calling directly os.urandom(). Rename also genrand_int32() to genrand_uint32(), since it returns an unsigned 32-bit integer, not a signed integer. The _random module is now built with Py_BUILD_CORE_MODULE defined.
1 parent 22386bb commit 9f5fe79

File tree

10 files changed

+177
-12
lines changed

10 files changed

+177
-12
lines changed

Doc/library/random.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ Bookkeeping functions
112112
:meth:`randrange` to handle arbitrarily large ranges.
113113

114114

115+
.. function:: randbytes(n)
116+
117+
Generate *n* random bytes.
118+
119+
.. versionadded:: 3.9
120+
121+
115122
Functions for integers
116123
----------------------
117124

Doc/whatsnew/3.9.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ The documentation string is now shown not only for class, function,
353353
method etc, but for any object that has its own ``__doc__`` attribute.
354354
(Contributed by Serhiy Storchaka in :issue:`40257`.)
355355

356+
random
357+
------
358+
359+
Add a new :attr:`random.Random.randbytes` method: generate random bytes.
360+
(Contributed by Victor Stinner in :issue:`40286`.)
361+
356362
signal
357363
------
358364

Lib/random.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,12 @@ def getrandbits(self, k):
739739
x = int.from_bytes(_urandom(numbytes), 'big')
740740
return x >> (numbytes * 8 - k) # trim excess bits
741741

742+
def randbytes(self, n):
743+
"""Generate n random bytes."""
744+
# os.urandom(n) fails with ValueError for n < 0
745+
# and returns an empty bytes string for n == 0.
746+
return _urandom(n)
747+
742748
def seed(self, *args, **kwds):
743749
"Stub method. Not used for a system random number generator."
744750
return None
@@ -819,6 +825,7 @@ def _test(N=2000):
819825
getstate = _inst.getstate
820826
setstate = _inst.setstate
821827
getrandbits = _inst.getrandbits
828+
randbytes = _inst.randbytes
822829

823830
if hasattr(_os, "fork"):
824831
_os.register_at_fork(after_in_child=_inst.seed)

Lib/secrets.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import base64
1616
import binascii
17-
import os
1817

1918
from hmac import compare_digest
2019
from random import SystemRandom
@@ -44,7 +43,7 @@ def token_bytes(nbytes=None):
4443
"""
4544
if nbytes is None:
4645
nbytes = DEFAULT_ENTROPY
47-
return os.urandom(nbytes)
46+
return _sysrand.randbytes(nbytes)
4847

4948
def token_hex(nbytes=None):
5049
"""Return a random text string, in hexadecimal.

Lib/test/test_random.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,22 @@ def test_bug_9025(self):
291291
k = sum(randrange(6755399441055744) % 3 == 2 for i in range(n))
292292
self.assertTrue(0.30 < k/n < .37, (k/n))
293293

294+
def test_randbytes(self):
295+
# Verify ranges
296+
for n in range(1, 10):
297+
data = self.gen.randbytes(n)
298+
self.assertEqual(type(data), bytes)
299+
self.assertEqual(len(data), n)
300+
301+
self.assertEqual(self.gen.randbytes(0), b'')
302+
303+
# Verify argument checking
304+
self.assertRaises(TypeError, self.gen.randbytes)
305+
self.assertRaises(TypeError, self.gen.randbytes, 1, 2)
306+
self.assertRaises(ValueError, self.gen.randbytes, -1)
307+
self.assertRaises(TypeError, self.gen.randbytes, 1.0)
308+
309+
294310
try:
295311
random.SystemRandom().random()
296312
except NotImplementedError:
@@ -747,6 +763,41 @@ def test_choices_algorithms(self):
747763
c = self.gen.choices(population, cum_weights=cum_weights, k=10000)
748764
self.assertEqual(a, c)
749765

766+
def test_randbytes(self):
767+
super().test_randbytes()
768+
769+
# Mersenne Twister randbytes() is deterministic
770+
# and does not depend on the endian and bitness.
771+
seed = 8675309
772+
expected = b'f\xf9\xa836\xd0\xa4\xf4\x82\x9f\x8f\x19\xf0eo\x02'
773+
774+
self.gen.seed(seed)
775+
self.assertEqual(self.gen.randbytes(16), expected)
776+
777+
# randbytes(0) must not consume any entropy
778+
self.gen.seed(seed)
779+
self.assertEqual(self.gen.randbytes(0), b'')
780+
self.assertEqual(self.gen.randbytes(16), expected)
781+
782+
# Four randbytes(4) calls give the same output than randbytes(16)
783+
self.gen.seed(seed)
784+
self.assertEqual(b''.join([self.gen.randbytes(4) for _ in range(4)]),
785+
expected)
786+
787+
# Each randbytes(2) or randbytes(3) call consumes 4 bytes of entropy
788+
self.gen.seed(seed)
789+
expected2 = b''.join(expected[i:i + 2]
790+
for i in range(0, len(expected), 4))
791+
self.assertEqual(b''.join(self.gen.randbytes(2) for _ in range(4)),
792+
expected2)
793+
794+
self.gen.seed(seed)
795+
expected3 = b''.join(expected[i:i + 3]
796+
for i in range(0, len(expected), 4))
797+
self.assertEqual(b''.join(self.gen.randbytes(3) for _ in range(4)),
798+
expected3)
799+
800+
750801
def gamma(z, sqrt2pi=(2.0*pi)**0.5):
751802
# Reflection to right half of complex plane
752803
if z < 0.5:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`random.randbytes` function and
2+
:meth:`random.Random.randbytes` method to generate random bytes.

Modules/Setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ _symtable symtablemodule.c
174174
#_weakref _weakref.c # basic weak reference support
175175
#_testcapi _testcapimodule.c # Python C API test module
176176
#_testinternalcapi _testinternalcapi.c -I$(srcdir)/Include/internal -DPy_BUILD_CORE_MODULE # Python internal C API test module
177-
#_random _randommodule.c # Random number generator
177+
#_random _randommodule.c -DPy_BUILD_CORE_MODULE # Random number generator
178178
#_elementtree -I$(srcdir)/Modules/expat -DHAVE_EXPAT_CONFIG_H -DUSE_PYEXPAT_CAPI _elementtree.c # elementtree accelerator
179179
#_pickle _pickle.c # pickle accelerator
180180
#_datetime _datetimemodule.c # datetime accelerator

Modules/_randommodule.c

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* renamed genrand_res53() to random_random() and wrapped
1212
in python calling/return code.
1313
14-
* genrand_int32() and the helper functions, init_genrand()
14+
* genrand_uint32() and the helper functions, init_genrand()
1515
and init_by_array(), were declared static, wrapped in
1616
Python calling/return code. also, their global data
1717
references were replaced with structure references.
@@ -67,9 +67,9 @@
6767
/* ---------------------------------------------------------------*/
6868

6969
#include "Python.h"
70-
#include <time.h> /* for seeding to current time */
70+
#include "pycore_byteswap.h" // _Py_bswap32()
7171
#ifdef HAVE_PROCESS_H
72-
# include <process.h> /* needed for getpid() */
72+
# include <process.h> // getpid()
7373
#endif
7474

7575
/* Period parameters -- These are all magic. Don't change. */
@@ -116,7 +116,7 @@ class _random.Random "RandomObject *" "&Random_Type"
116116

117117
/* generates a random number on [0,0xffffffff]-interval */
118118
static uint32_t
119-
genrand_int32(RandomObject *self)
119+
genrand_uint32(RandomObject *self)
120120
{
121121
uint32_t y;
122122
static const uint32_t mag01[2] = {0x0U, MATRIX_A};
@@ -171,7 +171,7 @@ static PyObject *
171171
_random_Random_random_impl(RandomObject *self)
172172
/*[clinic end generated code: output=117ff99ee53d755c input=afb2a59cbbb00349]*/
173173
{
174-
uint32_t a=genrand_int32(self)>>5, b=genrand_int32(self)>>6;
174+
uint32_t a=genrand_uint32(self)>>5, b=genrand_uint32(self)>>6;
175175
return PyFloat_FromDouble((a*67108864.0+b)*(1.0/9007199254740992.0));
176176
}
177177

@@ -481,7 +481,7 @@ _random_Random_getrandbits_impl(RandomObject *self, int k)
481481
}
482482

483483
if (k <= 32) /* Fast path */
484-
return PyLong_FromUnsignedLong(genrand_int32(self) >> (32 - k));
484+
return PyLong_FromUnsignedLong(genrand_uint32(self) >> (32 - k));
485485

486486
words = (k - 1) / 32 + 1;
487487
wordarray = (uint32_t *)PyMem_Malloc(words * 4);
@@ -498,7 +498,7 @@ _random_Random_getrandbits_impl(RandomObject *self, int k)
498498
for (i = words - 1; i >= 0; i--, k -= 32)
499499
#endif
500500
{
501-
r = genrand_int32(self);
501+
r = genrand_uint32(self);
502502
if (k < 32)
503503
r >>= (32 - k); /* Drop least significant bits */
504504
wordarray[i] = r;
@@ -510,6 +510,56 @@ _random_Random_getrandbits_impl(RandomObject *self, int k)
510510
return result;
511511
}
512512

513+
/*[clinic input]
514+
515+
_random.Random.randbytes
516+
517+
self: self(type="RandomObject *")
518+
n: Py_ssize_t
519+
/
520+
521+
Generate n random bytes.
522+
[clinic start generated code]*/
523+
524+
static PyObject *
525+
_random_Random_randbytes_impl(RandomObject *self, Py_ssize_t n)
526+
/*[clinic end generated code: output=67a28548079a17ea input=7ba658a24150d233]*/
527+
{
528+
if (n < 0) {
529+
PyErr_SetString(PyExc_ValueError,
530+
"number of bytes must be non-negative");
531+
return NULL;
532+
}
533+
534+
if (n == 0) {
535+
/* Don't consume any entropy */
536+
return PyBytes_FromStringAndSize(NULL, 0);
537+
}
538+
539+
PyObject *bytes = PyBytes_FromStringAndSize(NULL, n);
540+
if (bytes == NULL) {
541+
return NULL;
542+
}
543+
uint8_t *ptr = (uint8_t *)PyBytes_AS_STRING(bytes);
544+
545+
do {
546+
uint32_t word = genrand_uint32(self);
547+
#if PY_LITTLE_ENDIAN
548+
/* Convert to big endian */
549+
word = _Py_bswap32(word);
550+
#endif
551+
if (n < 4) {
552+
memcpy(ptr, &word, n);
553+
break;
554+
}
555+
memcpy(ptr, &word, 4);
556+
ptr += 4;
557+
n -= 4;
558+
} while (n);
559+
560+
return bytes;
561+
}
562+
513563
static PyObject *
514564
random_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
515565
{
@@ -539,6 +589,7 @@ static PyMethodDef random_methods[] = {
539589
_RANDOM_RANDOM_GETSTATE_METHODDEF
540590
_RANDOM_RANDOM_SETSTATE_METHODDEF
541591
_RANDOM_RANDOM_GETRANDBITS_METHODDEF
592+
_RANDOM_RANDOM_RANDBYTES_METHODDEF
542593
{NULL, NULL} /* sentinel */
543594
};
544595

Modules/clinic/_randommodule.c.h

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,8 @@ def detect_simple_extensions(self):
808808
self.add(Extension('_datetime', ['_datetimemodule.c'],
809809
libraries=['m']))
810810
# random number generator implemented in C
811-
self.add(Extension("_random", ["_randommodule.c"]))
811+
self.add(Extension("_random", ["_randommodule.c"],
812+
extra_compile_args=['-DPy_BUILD_CORE_MODULE']))
812813
# bisect
813814
self.add(Extension("_bisect", ["_bisectmodule.c"]))
814815
# heapq

0 commit comments

Comments
 (0)