-
-
Save glyph/ceca96100a3049fefea6f2035abbd9ea to your computer and use it in GitHub Desktop.
created by https://github.com/tr3buchet/gister
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Sometimes you want randomness that is I{unpredictable}, but still | |
I{repeatable}, and derived from a I{known}, I{human memorable} start point. | |
Before I continue: for cryptographic randomness, this is totally unsuitable. | |
Cryptographic randomness must be, above all, unpredictable, and repeatability | |
is the enemy of that. It should come from the operating system so that | |
cryptographic techniques for ensuring unpredictablility without knowing the | |
internal state are mixed with randomness derived from hardware to determine | |
that initial state. So if you're looking to do something with the Python | |
"random" object's interface that is I{in any way} security-relevant, you want | |
L{random.SystemRandom}; if you want random bytes, you want L{os.urandom}. | |
Now that we have accepted that you will I{never, ever} use this module for | |
security purposes: sometimes, particularly in the context of games, | |
particularly procedural generation of levels, it's handy to have the type of | |
randomness I'm describing. Many games (Minecraft and the .hack// series being | |
two of my favorites) use pseudo-random procedural generation to great effect. | |
The Python standard library's random number I{interface} is incredibly | |
convenient for this sort of application; it has a number of different random | |
distributions that are interesting, as well as utilities like "shuffle" whose | |
applications to games are self-evident. | |
However, the Python standard library's random number I{implementation} doesn't | |
quite fit. First, its PRNG algorithm (Mersenne Twister) is not quite | |
unpredictable: if you can observe its outputs, you can eventually U{derive its | |
inputs <https://en.wikipedia.org/wiki/Mersenne_Twister#Alternatives>}, which | |
might allow some players to cheat. Nor is it quite repeatable, when used with | |
human-memorable values; due to an U{unfortunate bug | |
<https://bugs.python.org/issue27706>}, you have to convert your strings into | |
integers yourself somehow before they're usable as stable seeds. | |
MIT license, (C) glyph; if it breaks you can keep both halves. | |
""" | |
from __future__ import unicode_literals | |
from random import ( | |
Random as _IfOnlyCompositionWerePossible, | |
BPF as _BPF, RECIP_BPF as _RECIP_BPF | |
) | |
from os import urandom | |
from binascii import hexlify, unhexlify | |
from cryptography.hazmat.primitives.ciphers import ( | |
Cipher, algorithms, modes | |
) | |
from cryptography.hazmat.backends import default_backend | |
from cryptography.utils import int_from_bytes, int_to_bytes | |
from hashlib import sha256 | |
def _bytes_for_bits(bits): | |
""" | |
How many bytes do I need to read to get the given number of bits of | |
entropy? | |
""" | |
bits_per_byte = 8 | |
return (bits + (bits_per_byte-1)) // bits_per_byte | |
class _StreamRandom(_IfOnlyCompositionWerePossible, object): | |
""" | |
A L{_StreamRandom} converts a stream of bytes into an object that has the | |
same useful methods as a standard library L{random.Random}. | |
""" | |
_stream_from_seed = None | |
@classmethod | |
def create(cls, seed, stream_from_seed): | |
""" | |
@param seed: an object usable by C{stream_from_seed} | |
@param stream_from_seed: a 1-argument callable taking an object like | |
C{seed} that returns a readable byte stream (a file-like object | |
with C{read->bytes} and C{seek} methods). | |
""" | |
self = _StreamRandom.__new__(cls) | |
self._stream_from_seed = stream_from_seed | |
self.__init__(seed) | |
return self | |
def seed(self, a=None): | |
""" | |
Create a new stream from the given seed. | |
""" | |
if self._stream_from_seed is None: | |
# serialization annoyance | |
return | |
if a is None: | |
a = urandom(16) | |
self._stream = self._stream_from_seed(a) | |
def random(self): | |
""" | |
Get the next random number in the range [0.0, 1.0). | |
""" | |
return self.getrandbits(_BPF) * _RECIP_BPF | |
def getrandbits(self, k): | |
""" | |
Get some random bits. This is the primitive upon which all | |
higher-level functions are built. | |
@return: an integer containing C{k} random bits | |
@rtype: L{int} or L{long} | |
""" | |
if k != int(k): | |
raise TypeError('k must be an integer') | |
if not k > 0: | |
raise ValueError('k must be positive') | |
octet_count = _bytes_for_bits(k) | |
octets = self._stream.read(octet_count) | |
if len(octets) != octet_count: | |
raise RuntimeError("out of entropy") | |
x = int_from_bytes(octets, 'big') | |
return x >> (octet_count * 8 - k) | |
def jumpahead(self, n): | |
""" | |
Jump ahead in the stream as if C{random} had been called C{n} times. | |
""" | |
self._stream.seek(n * 7, 1) | |
def getstate(self): | |
""" | |
Get the internal state necessary to serialize this object. | |
""" | |
return (self._stream, self._stream_from_seed) | |
def setstate(self, state): | |
""" | |
Unserialize this object from the given state, previously serialized by | |
C{getstate}. | |
""" | |
self._stream, self._stream_from_seed = state | |
class _AESCTRStream(object): | |
_octets_per_block = 16 | |
def __init__(self, key): | |
self._key = key | |
self.seek(0) | |
def __getstate__(self): | |
return self._key, self._pos | |
def __setstate__(self, keypos): | |
key, pos = keypos | |
self._key = key | |
self.seek(pos) | |
@classmethod | |
def from_seed(cls, seed): | |
if type(seed) is type(u''): | |
seed = seed.encode("utf-8") | |
return cls(sha256(seed).digest()[:16]) | |
def seek(self, n, whence=0): | |
if whence == 0: | |
goal = n | |
elif whence == 1: | |
goal = self._pos + n | |
else: | |
raise ValueError("SEEK_END not supported; keystreams are big.") | |
closest_block, beyond = divmod(goal, self._octets_per_block) | |
self._remaining = b'' | |
self._pos = closest_block * self._octets_per_block | |
self._encryptor = Cipher( | |
algorithms.AES(self._key), | |
modes.CTR(int_to_bytes(closest_block, self._octets_per_block)), | |
backend=default_backend() | |
).encryptor() | |
self.read(beyond) | |
def tell(self): | |
return self._pos | |
def read(self, n): | |
self._pos += n | |
result = b"" | |
while n: | |
if not self._remaining: | |
self._remaining += self._encryptor.update( | |
int_to_bytes(0, self._octets_per_block)) | |
more, self._remaining = self._remaining[:n], self._remaining[n:] | |
result += more | |
n -= len(more) | |
if n < 0: | |
raise RuntimeError("shouldn't be possible") | |
return result | |
def game_random(seed=None): | |
""" | |
Create a deterministic, but unpredictable random number generator. | |
@param seed: A random seed to start from, or L{None} for a random | |
start-point. | |
@type seed: L{six.text_type} or L{bytes} | |
@return: An L{random.Random}-like object, whose C{seed} method takes the | |
same type as C{seed}. | |
""" | |
if seed is None: | |
seed = urandom(16) | |
return _StreamRandom.create(seed, _AESCTRStream.from_seed) | |
def save(game_random_object): | |
""" | |
Preserve the internal state of L{game_random}. This will not contain the | |
seed itself, but it will contain all derived values necessary to | |
reconstruct the same deterministic random number generator. | |
@param game_random_object: The object to save. | |
@type game_random_object: An object returned by L{game_random} | |
""" | |
return { | |
"position": game_random_object._stream._pos, | |
"key": hexlify(game_random_object._stream._key).decode("ascii"), | |
} | |
def load(saved_object): | |
""" | |
Reconstruct the same kind of object created by L{game_random} from the | |
value returned by L{save}. | |
@param saved_object: a serialization-friendly data value | |
@type saved_object: whatever L{save} returns | |
@return: A L{random.Random}-like object. | |
""" | |
position = saved_object["position"] | |
key = unhexlify(saved_object["key"]) | |
stream = _AESCTRStream(key) | |
stream.seek(position) | |
result = _StreamRandom() | |
result._stream_from_seed = _AESCTRStream.from_seed | |
result._stream = stream | |
return result | |
__all__ = [ | |
'game_random', | |
'save', | |
'load', | |
] | |
if __name__ == '__main__': | |
for rnd in game_random("hello, world"), game_random(): | |
print("seed...") | |
for each in range(10): | |
print(rnd.random()) | |
import json | |
saved = json.dumps(save(rnd)) | |
rnd2 = load(json.loads(saved)) | |
for each in range(10): | |
print(rnd.random()) | |
print(rnd2.random()) | |
You had me at _IfOnlyCompositionWerePossible
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just to properly calibrate how much everyone should trust this: