Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@glyph
Created August 8, 2016 09:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save glyph/ceca96100a3049fefea6f2035abbd9ea to your computer and use it in GitHub Desktop.
Save glyph/ceca96100a3049fefea6f2035abbd9ea to your computer and use it in GitHub Desktop.
"""
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())
@glyph
Copy link
Author

glyph commented Aug 8, 2016

Just to properly calibrate how much everyone should trust this:
I am so smart

@tenth10th
Copy link

You had me at _IfOnlyCompositionWerePossible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment