"""
SHA-256 PRNG prototype in Python
"""
import numpy as np
import sys
import struct
# Import base class for PRNGs
import random
# Import library of cryptographic hash functions
import hashlib
# Define useful constants
BPF = 53 # Number of bits in a float
RECIP_BPF = 2**-BPF
HASHLEN = 256 # Number of bits in a hash output
RECIP_HASHLEN = 2**-HASHLEN
################################################################################
############################## Int from Hash ###################################
################################################################################
def int_from_hash_py2(hash):
'''
Convert byte(s) to ints, specific for Python versions < 3.
Parameters
----------
hash : bytes
Hash or list of hashes to convert to integers
Returns
-------
int or list ndarray of ints
'''
if isinstance(hash, list):
hash_int = np.array([int(h.encode('hex'), 16) for h in hash])
else:
hash_int = int(hash.encode('hex'), 16)
return hash_int
def int_from_hash_py3(hash):
'''
Convert byte(s) to ints, specific for Python 3.
Parameters
----------
hash : bytes
Hash or list of hashes to convert to integers
Returns
-------
int or list ndarray of ints
'''
if isinstance(hash, list):
hash_int = np.array([int.from_bytes(h, 'big') for h in hash])
else:
hash_int = int.from_bytes(hash, 'big')
return hash_int
if sys.version_info[0] < 3:
int_from_hash = int_from_hash_py2
else:
int_from_hash = int_from_hash_py3
################################################################################
############################## SHA-256 Class ###################################
################################################################################
[docs]class SHA256(random.Random):
"""
PRNG based on the SHA-256 cryptographic hash function.
"""
def __init__(self, seed=None):
"""
Initialize an instance of the SHA-256 PRNG.
Parameters
----------
seed : {None, int, string} (optional)
Random seed used to initialize the PRNG. It can be an integer of arbitrary length,
a string of arbitrary length, or `None`. Default is `None`.
"""
self.seed(seed)
self.hashfun = "SHA-256"
self._basehash()
def __repr__(self):
"""
>>> r = SHA256(5)
>>> repr(r)
'SHA256 PRNG. seed: 5 counter: 0 randbits_remaining: 0'
>>> str(r)
'SHA256 PRNG. seed: 5 counter: 0 randbits_remaining: 0'
"""
stringrepr = self.__class__.__name__ + " PRNG. seed: " + \
str(self.baseseed) + " counter: " + str(self.counter) + \
" randbits_remaining: " + str(self.randbits_remaining)
return stringrepr
def _basehash(self):
"""
Initialize the SHA256 hash function with given seed
"""
if self.baseseed is not None:
hashinput = (str(self.baseseed) + ',').encode()
self.basehash = hashlib.sha256(hashinput)
else:
self.basehash = None
[docs] def seed(self, baseseed=None):
"""
Initialize internal seed and hashable object with counter 0.
Parameters
----------
baseseed : {None, int, string} (optional)
Random seed used to initialize the PRNG. It can be an integer of arbitrary length,
a string of arbitrary length, or `None`. Default is `None`.
counter : int (optional)
Integer that counts how many times the PRNG has been called. The counter
is used to update the internal state after each step. Default is 0.
"""
if not hasattr(self, 'baseseed') or baseseed != self.baseseed:
self.baseseed = baseseed
self._basehash()
self.counter = 0
self.randbits = None
self.randbits_remaining = 0
[docs] def setstate(self, baseseed=None, counter=0, randbits_remaining=0):
"""
Set the state (seed and counter)
Parameters
----------
baseseed : {None, int, string} (optional)
Random seed used to initialize the PRNG. It can be an integer of arbitrary length,
a string of arbitrary length, or `None`. Default is `None`.
counter : int (optional)
Integer that counts how many times the PRNG has been called. The counter
is used to update the internal state after each step. Default is 0.
"""
(self.baseseed, self.counter) = (baseseed, counter)
self._basehash()
self.basehash.update(b'\x00'*counter)
self.randbits_remaining = randbits_remaining
[docs] def getstate(self):
"""
Get the current state of the PRNG
"""
return (self.baseseed, self.counter, self.randbits_remaining)
[docs] def jumpahead(self, n):
"""
Jump ahead n steps in the period
>>> r = SHA256(5)
>>> r.jumpahead(5)
>>> repr(r)
'SHA256 PRNG with seed 5 and counter 5'
"""
self.counter += n
self.basehash.update(b'\x00'*n)
[docs] def next(self):
"""
Increment the counter and basehash by one
"""
self.jumpahead(1)
[docs] def nextRandom(self):
"""
Generate the next hash value
>>> r = SHA256(12345678901234567890)
>>> r.next()
>>> r.nextRandom()
4da594a8ab6064d666eab2bdf20cb4480e819e0c3102ca353de57caae1d11fd1
"""
# Apply SHA-256, interpreting digest output as integer
# to yield 256-bit integer (a python "long integer")
hash_output = self.basehash.digest()
self.next()
return hash_output
[docs] def random(self, size=None):
"""
Generate random numbers between 0 and 1.
size controls the number of ints generated. If size=None, just one is produced.
The following tests match the output of Ron's and Philip's implementations.
>>> r = SHA256(12345678901234567890)
>>> r.random(2)
array([0.9272915426537484, 0.1916135318809483], dtype=object)
>>> r.random((2, 2))
array([[0.5846237047310486, 0.18694233108130068],
[0.9022661737961881, 0.052310932788987144]], dtype=object)
Parameters
----------
size : {int, tuple, None}
If None (default), return a single random number.
If size is an int, return that many random numbers.
If size is a tuple, it determines the shape of an array
filled with random numbers.
"""
if size == None:
hash_output = self.nextRandom()
return int_from_hash(hash_output)*RECIP_HASHLEN
else:
size2 = np.prod(size)
hash_output = [self.nextRandom() for i in range(size2)]
res = int_from_hash(hash_output)*RECIP_HASHLEN
return np.reshape(res, size)
[docs] def randint_trunc(self, a, b, size=None):
"""
Deprecated. For large values of (b-a), this algorithm does not produce integers
uniformly at random.
Generate random integers between a (inclusive) and b (exclusive).
size controls the number of ints generated. If size=None, just one is produced.
>>> r = SHA256(12345678901234567890)
>>> r.randint_trunc(0, 5, size=3)
array([0, 0, 0])
Parameters
----------
a : int
lower limit (included in samples)
b : int
upper limit (not included in samples)
size : {int, tuple, None}
If None (default), return a single random number.
If size is an int, return that many random numbers.
If size is a tuple, it determines the shape of an array
filled with random numbers.
"""
assert a <= b, "lower and upper limits are switched"
if size == None:
return a + (int_from_hash(self.nextRandom()) % (b-a))
else:
return np.reshape(np.array([a + (int_from_hash(self.nextRandom()) % (b-a)) \
for i in range(np.prod(size))]), size)
[docs] def getrandbits(self, k):
"""
Generate k pseudorandom bits.
If self.randbits contains at least k bits, returns k of those bits and removes them.
If self.randbits has fewer than k bits, calls self.nextRandom() as many times as needed to
populate self.randbits with at least k random bits, returns those k, and keeps
any remaining bits in self.randbits
Parameters
----------
k : int
number of pseudorandom bits
"""
if self.randbits is None: # initialize the cache
self.randbits = int_from_hash(self.nextRandom())
self.randbits_remaining = HASHLEN
while k > self.randbits_remaining: # pre-pend more random bits
# accounts for leading 0s
self.randbits = (int_from_hash(self.nextRandom()) << \
self.randbits_remaining | self.randbits)
self.randbits_remaining = self.randbits_remaining + HASHLEN
val = (self.randbits & int(2**k-1)) # harvest least significant k bits
self.randbits_remaining = self.randbits_remaining - k
self.randbits = self.randbits >> k # discard the k harvested bits
return val
[docs] def randbelow_from_randbits(self, n):
"""
Generate a random integer between 0 (inclusive) and n (exclusive).
Raises ValueError if n==0.
Parameters
----------
n : int
upper limit
"""
k = int(n-1).bit_length()
r = self.getrandbits(k) # 0 <= r < 2**k
while int(r) >= n:
r = self.getrandbits(k)
return int(r)
[docs] def randint(self, a, b, size=None):
"""
Generate random integers between a (inclusive) and b (exclusive).
size controls the number of ints generated. If size=None, just one is produced.
>>> r = SHA256(12345678901234567890)
>>> r.randint(0, 5, size=3)
array([3, 2, 4])
Parameters
----------
a : int
lower limit (included in samples)
b : int
upper limit (not included in samples)
size : {int, tuple, None}
If None (default), return a single random number.
If size is an int, return that many random numbers.
If size is a tuple, it determines the shape of an array
filled with random numbers.
"""
assert a <= b, "lower and upper limits are switched"
if size == None:
return a + self.randbelow_from_randbits(b-a)
else:
return np.reshape(np.array([a + self.randbelow_from_randbits(b-a) \
for i in range(np.prod(size))]), size)