django-ddp/dddp/alea.py

163 lines
4 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python
"""
Alea PRNG.
This implementation of Alea defaults to a more secure initial internal state.
>>> r1, r2 = Alea(), Alea()
>>> assert r1.state != r2.state, 'r1: %r, r2: %r' % (r1.state, r2.state)
>>> random = Alea("my", 3, "seeds")
>>> (random.s0, random.s1, random.s2)
(0.23922116006724536, 0.6147655111271888, 0.3493568613193929)
>>> random()
0.30802189325913787
>>> random()
0.5190450621303171
>>> random()
0.43635262292809784
>>> random = Alea("my", 3, "seeds")
>>> random()
0.30802189325913787
>>> random = Alea("my", 3, "seeds")
>>> random.random_string(17, UNMISTAKABLE) == 'JYRduBwQtjpeCkqP7'
True
>>> random.random_string(17, UNMISTAKABLE) == 'HLxYtpZBtSain84zj'
True
>>> random.random_string(17, UNMISTAKABLE) == 's9XrbWaDC4yCL5NCW'
True
>>> random.random_string(17, UNMISTAKABLE) == 'SCiymgNnZpwda9vSH'
True
>>> random.random_string(17, UNMISTAKABLE) == 'hui3ThSoZrFrdFDTT'
True
>>> random = Alea("my", 3, "seeds")
>>> random.random_string(43, BASE64) == \
'tHBM5k8z4TZOmU0zgsv9H4ZIl4CJSXic_T3iF2KFJnm'
True
"""
from __future__ import unicode_literals
from math import floor
import os
import random
import time
UNMISTAKABLE = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'
BASE64 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
class Mash(object):
"""
`Mash` hasing algorithm.
>>> mash = Mash()
>>> mash(' ')
0.8633289230056107
>>> mash(' ')
0.15019597788341343
>>> mash(' ')
0.9176952994894236
"""
def __init__(self):
"""Initialise state."""
self.n = 0xefc8249d
def __call__(self, data):
"""Return mash, updating internal state."""
data = str(data)
for byte in data:
self.n += ord(byte)
h = 0.02519603282416938 * self.n
self.n = floor(h)
h -= self.n
h *= self.n
self.n = floor(h)
h -= self.n
self.n += h * 0x100000000
res = self.n * 2.3283064365386963e-10 # 2^-32
return res
class Alea(object):
"""Alea stateful PRNG."""
c = None
s0 = None
s1 = None
s2 = None
def __init__(self, *args):
"""Initialise Alea state from seeds (args)."""
self.seed(args)
def seed(self, values):
"""Seed internal state from supplied values."""
if not values:
# Meteor uses epoch seconds as the seed if no args supplied, we use
# a much more secure seed by default to avoid hash collisions.
seed_ids = [int, str, random, self, values, self.__class__]
random.shuffle(seed_ids)
values = list(map(id, seed_ids)) + [time.time(), os.urandom(512)]
mash = Mash()
self.c = 1
self.s0 = mash(' ')
self.s1 = mash(' ')
self.s2 = mash(' ')
for val in values:
self.s0 -= mash(val)
if self.s0 < 0:
self.s0 += 1
self.s1 -= mash(val)
if self.s1 < 0:
self.s1 += 1
self.s2 -= mash(val)
if self.s2 < 0:
self.s2 += 1
@property
def state(self):
"""Return internal state, useful for testing."""
return {'c': self.c, 's0': self.s0, 's1': self.s1, 's2': self.s2}
def __call__(self):
"""Get the next psuedo random number, updating state."""
t = 2091639 * self.s0 + self.c * 2.3283064365386963e-10 # 2^-32
self.c = floor(t)
self.s0 = self.s1
self.s1 = self.s2
self.s2 = t - self.c
return self.s2
def choice(self, seq):
"""Choose an element from the sequence `seq`."""
return seq[int(self() * len(seq))]
def random_string(self, length, alphabet):
"""Return string of `length` elements chosen from `alphabet`."""
return ''.join(
self.choice(alphabet) for n in range(length)
)
def hex_string(self, digits):
"""Return a hex string of `digits` length."""
return self.random_string(digits, '0123456789abcdef')