Class: PoolOfEntropy::CorePRNG

Inherits:
Object
  • Object
show all
Defined in:
lib/pool_of_entropy/core_prng.rb

Overview

This class implements a random number generator based on SHA-512

An object of the class has internal state that is modified on each call to #update or any #read_... method (internally the #read_... methods call #update). The #read_... methods generate a pseudo-random number from the current state pool using SHA-512 and use it in the return value. It is not feasible to determine the internal state from the pseudo-random data received, and not possible to manipulate results from the PRNG in a predictable manner without knowing the internal state.

Examples:

Using default internal state, initialised using SecureRandom.random_bytes

prng = PoolOfEntropy::CorePRNG.new
prng.read_bytes
# E.g. => "]\x12\x9E\xF5\x17\xF3\xC2\x1A\x15\xDFu]\x95\nd\x12"
prng.read_hex
# E.g. => "a0e00d2848242ec49e0a15ef411ba647"
prng.read_bignum
# E.g. => 33857278877368906880463811096418580004
prng.read_float
# E.g. => 0.6619838265836278
prng.generate_integer( 20 )
# E.g. => 7

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(size = 1, initial_state = SecureRandom.random_bytes(64 * Integer(size)), mix_block_id = 0) ⇒ PoolOfEntropy::CorePRNG

Creates a new random number source. All parameters are optional.

Parameters:

  • size (Integer) (defaults to: 1)

    Number of 64-byte blocks, between 1 and 256 (16KB)

  • initial_state (String) (defaults to: SecureRandom.random_bytes(64 * Integer(size)))

    Sets contents of state pool (default uses SecureRandom)

  • mix_block_id (Integer) (defaults to: 0)


36
37
38
39
40
# File 'lib/pool_of_entropy/core_prng.rb', line 36

def initialize(size = 1, initial_state = SecureRandom.random_bytes(64 * Integer(size)), mix_block_id = 0)
  @size = validate_size(size)
  @state = validate_state(initial_state, size)
  @mix_block_id = Integer(mix_block_id) % @size
end

Instance Attribute Details

#mix_block_idInteger (readonly)

Identifies the next 64-byte block in the pool that will be altered by a read or update process.

Returns:

  • (Integer)


49
50
51
# File 'lib/pool_of_entropy/core_prng.rb', line 49

def mix_block_id
  @mix_block_id
end

#sizeInteger (readonly)

The number of 64-byte blocks used in the internal state pool.

Returns:

  • (Integer)


44
45
46
# File 'lib/pool_of_entropy/core_prng.rb', line 44

def size
  @size
end

Instance Method Details

#clonePoolOfEntropy::CorePRNG

The clone of a PoolOfEntropy::CorePRNG object includes separate copy of internal state



61
62
63
# File 'lib/pool_of_entropy/core_prng.rb', line 61

def clone
  PoolOfEntropy::CorePRNG.new(size, state, mix_block_id)
end

#generate_integer(top, *adjustments) ⇒ Fixnum, Bignum

Statistically flat distribution from range (0...top). If necessary, it will read more data to ensure absolute fairness. This method can generate an unbiased distribution of Bignums up to roughly half the maximum bit size allowed by Ruby (i.e. much larger than 2**128 generated in a single read)

Parameters:

  • top (Fixnum, Bignum)

    upper bound of distribution, not inclusive

  • adjustments (Array<String>)

    mixed in using SHA-512, so that they affect the return value, but not internal state

Returns:

  • (Fixnum, Bignum)

    between 0 and top-1 inclusive



123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/pool_of_entropy/core_prng.rb', line 123

def generate_integer(top, *adjustments)
  power = 1
  sum = 0
  words = []

  loop do
    words = read_bytes(*adjustments).unpack('L>*') if words.empty?
    sum = ((2**32) * sum) + words.shift
    power *= 2**32
    lower_bound = sum * top / power
    break lower_bound if lower_bound == ((sum + 1) * top) / power
  end
end

#read_bignum(*adjustments) ⇒ Bignum, Fixnum

Statistically flat distribution from range 0...2**128

Parameters:

  • adjustments (Array<String>)

    mixed in using SHA-512, so that they affect the return value, but not internal state

Returns:

  • (Bignum, Fixnum)

    between 0 and 0xffffffffffffffffffffffffffffffff



101
102
103
104
# File 'lib/pool_of_entropy/core_prng.rb', line 101

def read_bignum(*adjustments)
  nums = read_bytes(*adjustments).unpack('Q>*')
  nums.inject(0) { |sum, v| (sum << 64) + v }
end

#read_bytes(*adjustments) ⇒ String

Statistically flat distribution of 128 bits (16 bytes)

Parameters:

  • adjustments (Array<String>)

    mixed in using SHA-512, so that they affect the return value, but not internal state

Returns:

  • (String)

    16 characters in ASCII-8BIT encoding



80
81
82
83
84
85
86
87
# File 'lib/pool_of_entropy/core_prng.rb', line 80

def read_bytes(*adjustments)
  raw_digest = Digest::SHA512.digest(@state)
  update(raw_digest)
  adjustments.compact.each do |adjust|
    raw_digest = Digest::SHA512.digest(raw_digest + adjust)
  end
  fold_bits(fold_bits(raw_digest))
end

#read_float(*adjustments) ⇒ Float

Statistically flat distribution from interval 0.0...1.0, with 53-bit precision

Parameters:

  • adjustments (Array<String>)

    mixed in using SHA-512, so that they affect the return value, but not internal state

Returns:

  • (Float)

    between 0.0 and 0.9999999999999999



110
111
112
113
# File 'lib/pool_of_entropy/core_prng.rb', line 110

def read_float(*adjustments)
  num = read_bytes(*adjustments).unpack1('Q>*') >> 11
  num.to_f / (2**53)
end

#read_hex(*adjustments) ⇒ String

Statistically flat distribution of 32 hex digits

Parameters:

  • adjustments (Array<String>)

    mixed in using SHA-512, so that they affect the return value, but not internal state

Returns:

  • (String)

    32 hex digits



93
94
95
# File 'lib/pool_of_entropy/core_prng.rb', line 93

def read_hex(*adjustments)
  read_bytes(*adjustments).unpack1('H*')
end

#statePoolOfEntropy::CorePRNG

A clone of the internal state pool. In combination with #mix_block_id, describes the whole PRNG. If this value is supplied to an end user, then they can easily predict future values of the PRNG.



55
56
57
# File 'lib/pool_of_entropy/core_prng.rb', line 55

def state
  @state.clone
end

#update(data) ⇒ nil

Mixes supplied data into the curent state. This is called internally by #read_... methods as well.

Parameters:

  • data (String)

    Data to be mixed. Note empty string '' and nil are equivalent and do change the state.

Returns:

  • (nil)


69
70
71
72
73
74
# File 'lib/pool_of_entropy/core_prng.rb', line 69

def update(data)
  new_block = Digest::SHA512.digest(@state + data.to_s)
  @state[64 * @mix_block_id, 64] = new_block
  @mix_block_id = (@mix_block_id + 1) % @size
  nil
end