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)


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

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)


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

def mix_block_id
  @mix_block_id
end

#sizeInteger (readonly)

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

Returns:

  • (Integer)


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

def size
  @size
end

Instance Method Details

#clonePoolOfEntropy::CorePRNG

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



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

def clone
  PoolOfEntropy::CorePRNG.new( self.size, self.state, self.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 return value, but not internal state

Returns:

  • (Fixnum, Bignum)

    between 0 and top-1 inclusive



117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/pool_of_entropy/core_prng.rb', line 117

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 return value, but not internal state

Returns:

  • (Bignum, Fixnum)

    between 0 and 0xffffffffffffffffffffffffffffffff



97
98
99
100
# File 'lib/pool_of_entropy/core_prng.rb', line 97

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 return value, but not internal state

Returns:

  • (String)

    16 characters in ASCII-8BIT encoding



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

def read_bytes *adjustments
  raw_digest = Digest::SHA512.digest( @state )
  self.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 return value, but not internal state

Returns:

  • (Float)

    between 0.0 and 0.9999999999999999



105
106
107
108
# File 'lib/pool_of_entropy/core_prng.rb', line 105

def read_float *adjustments
  num = read_bytes( *adjustments ).unpack('Q>*').first >> 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 return value, but not internal state

Returns:

  • (String)

    32 hex digits



90
91
92
# File 'lib/pool_of_entropy/core_prng.rb', line 90

def read_hex *adjustments
  read_bytes( *adjustments ).unpack('H*').first
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.



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

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)


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

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