pool_of_entropy 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/rspec.yml +23 -0
- data/.rubocop.yml +18 -0
- data/Gemfile +11 -1
- data/README.md +0 -4
- data/Rakefile +7 -5
- data/lib/pool_of_entropy/core_prng.rb +149 -143
- data/lib/pool_of_entropy/version.rb +3 -1
- data/lib/pool_of_entropy.rb +57 -57
- data/pool_of_entropy.gemspec +15 -16
- data/spec/pool_of_entropy/core_prng_spec.rb +386 -0
- data/spec/pool_of_entropy_spec.rb +264 -303
- data/spec/spec_helper.rb +49 -3
- metadata +12 -88
- data/.travis.yml +0 -7
- data/spec/core_prng_spec.rb +0 -543
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4cf2426e234e3234f458defe0d59aadda7b95e08b3161a3edc5c79fed8f829dc
|
|
4
|
+
data.tar.gz: 5573c75b6a5a565296c4271c1e32acc0e14989996bc3b85f660f94072f0e7d38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bb69625454353f670b3fe6e12a8cf8d70b2bf11aeeb145e6d3fea93a3a967f8bccb0a985bc3060b6c856a7d794bcc83a9c206f87a2b10f99ccc2af989b95c37
|
|
7
|
+
data.tar.gz: e03b6919f0ca301e74c3c7255a4e8302381f763e04f199dcc144d5e0a3601b560b9bdd70769e62dd74625e2d3e481b7eb9185c913814c7c29d4508284ea65cd0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: RSpec
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
fail-fast: false
|
|
11
|
+
matrix:
|
|
12
|
+
ruby-version:
|
|
13
|
+
- '3.3'
|
|
14
|
+
- '3.4'
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
- run: bundle exec rubocop
|
|
23
|
+
- run: bundle exec rspec
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rake
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
TargetRubyVersion: 3.3
|
|
7
|
+
NewCops: enable
|
|
8
|
+
|
|
9
|
+
Layout/LineLength:
|
|
10
|
+
Max: 120
|
|
11
|
+
|
|
12
|
+
Metrics/BlockLength:
|
|
13
|
+
Exclude:
|
|
14
|
+
- spec/**/*
|
|
15
|
+
AllowedMethods:
|
|
16
|
+
- context
|
|
17
|
+
- describe
|
|
18
|
+
- shared_examples
|
data/Gemfile
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
source 'https://rubygems.org'
|
|
2
4
|
|
|
3
|
-
# Dependencies are in pool_of_entropy.gemspec
|
|
4
5
|
gemspec
|
|
6
|
+
|
|
7
|
+
gem 'bundler', '>= 2.5', '< 5'
|
|
8
|
+
gem 'rake', '~> 13.2'
|
|
9
|
+
gem 'rspec', '~> 3.13'
|
|
10
|
+
gem 'rubocop', '~> 1.88'
|
|
11
|
+
gem 'rubocop-rake', '~> 0.7'
|
|
12
|
+
gem 'rubocop-rspec', '~> 3.10'
|
|
13
|
+
gem 'simplecov', '~> 1.0'
|
|
14
|
+
gem 'yard', '~> 0.9.37'
|
data/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# PoolOfEntropy
|
|
2
2
|
[](http://badge.fury.io/rb/pool_of_entropy)
|
|
3
|
-
[](http://travis-ci.org/neilslater/pool_of_entropy)
|
|
4
|
-
[](https://coveralls.io/r/neilslater/pool_of_entropy?branch=master)
|
|
5
|
-
[](https://codeclimate.com/github/neilslater/pool_of_entropy)
|
|
6
|
-
[](https://gemnasium.com/neilslater/pool_of_entropy)
|
|
7
3
|
|
|
8
4
|
PoolOfEntropy is a pseudo random number generator (PRNG) based on secure hashes,
|
|
9
5
|
intended to bring back the feeling of 'personal agency' that some gamers may feel when rolling
|
data/Rakefile
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
require "rspec/core/rake_task"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
desc 'PoolOfEntropy unit tests'
|
|
5
7
|
RSpec::Core::RakeTask.new(:test) do |t|
|
|
6
|
-
t.pattern =
|
|
8
|
+
t.pattern = 'spec/**/*_spec.rb'
|
|
7
9
|
t.verbose = false
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
task :
|
|
12
|
+
task default: [:test]
|
|
@@ -1,165 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'digest/sha2'
|
|
2
4
|
require 'securerandom'
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# prng.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
class
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# The number of 64-byte blocks used in the internal state pool.
|
|
42
|
-
# @return [Integer]
|
|
43
|
-
attr_reader :size
|
|
44
|
-
|
|
45
|
-
# Identifies the next 64-byte block in the pool that will be altered
|
|
46
|
-
# by a read or update process.
|
|
47
|
-
# @return [Integer]
|
|
48
|
-
attr_reader :mix_block_id
|
|
49
|
-
|
|
50
|
-
# A clone of the internal state pool. In combination with #mix_block_id, describes
|
|
51
|
-
# the whole PRNG. If this value is supplied to an end user, then they can easily
|
|
52
|
-
# predict future values of the PRNG.
|
|
53
|
-
# @return [PoolOfEntropy::CorePRNG]
|
|
54
|
-
def state
|
|
55
|
-
@state.clone
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# The clone of a PoolOfEntropy::CorePRNG object includes separate copy of internal state
|
|
59
|
-
# @return [PoolOfEntropy::CorePRNG]
|
|
60
|
-
def clone
|
|
61
|
-
PoolOfEntropy::CorePRNG.new( self.size, self.state, self.mix_block_id )
|
|
62
|
-
end
|
|
6
|
+
class PoolOfEntropy
|
|
7
|
+
# This class implements a random number generator based on SHA-512
|
|
8
|
+
#
|
|
9
|
+
# An object of the class has internal state that is modified on each call to
|
|
10
|
+
# #update or any #read_... method (internally the #read_... methods call #update).
|
|
11
|
+
# The #read_... methods generate a pseudo-random number from the current
|
|
12
|
+
# state pool using SHA-512 and use it in the return value. It is not feasible to
|
|
13
|
+
# determine the internal state from the pseudo-random data received, and not
|
|
14
|
+
# possible to manipulate results from the PRNG in a predictable manner without
|
|
15
|
+
# knowing the internal state.
|
|
16
|
+
#
|
|
17
|
+
# @example Using default internal state, initialised using SecureRandom.random_bytes
|
|
18
|
+
# prng = PoolOfEntropy::CorePRNG.new
|
|
19
|
+
# prng.read_bytes
|
|
20
|
+
# # E.g. => "]\x12\x9E\xF5\x17\xF3\xC2\x1A\x15\xDFu]\x95\nd\x12"
|
|
21
|
+
# prng.read_hex
|
|
22
|
+
# # E.g. => "a0e00d2848242ec49e0a15ef411ba647"
|
|
23
|
+
# prng.read_bignum
|
|
24
|
+
# # E.g. => 33857278877368906880463811096418580004
|
|
25
|
+
# prng.read_float
|
|
26
|
+
# # E.g. => 0.6619838265836278
|
|
27
|
+
# prng.generate_integer( 20 )
|
|
28
|
+
# # E.g. => 7
|
|
29
|
+
#
|
|
30
|
+
class CorePRNG
|
|
31
|
+
# Creates a new random number source. All parameters are optional.
|
|
32
|
+
# @param [Integer] size Number of 64-byte blocks, between 1 and 256 (16KB)
|
|
33
|
+
# @param [String] initial_state Sets contents of state pool (default uses SecureRandom)
|
|
34
|
+
# @param [Integer] mix_block_id
|
|
35
|
+
# @return [PoolOfEntropy::CorePRNG]
|
|
36
|
+
def initialize(size = 1, initial_state = SecureRandom.random_bytes(64 * Integer(size)), mix_block_id = 0)
|
|
37
|
+
@size = validate_size(size)
|
|
38
|
+
@state = validate_state(initial_state, size)
|
|
39
|
+
@mix_block_id = Integer(mix_block_id) % @size
|
|
40
|
+
end
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
42
|
+
# The number of 64-byte blocks used in the internal state pool.
|
|
43
|
+
# @return [Integer]
|
|
44
|
+
attr_reader :size
|
|
45
|
+
|
|
46
|
+
# Identifies the next 64-byte block in the pool that will be altered
|
|
47
|
+
# by a read or update process.
|
|
48
|
+
# @return [Integer]
|
|
49
|
+
attr_reader :mix_block_id
|
|
50
|
+
|
|
51
|
+
# A clone of the internal state pool. In combination with #mix_block_id, describes
|
|
52
|
+
# the whole PRNG. If this value is supplied to an end user, then they can easily
|
|
53
|
+
# predict future values of the PRNG.
|
|
54
|
+
# @return [PoolOfEntropy::CorePRNG]
|
|
55
|
+
def state
|
|
56
|
+
@state.clone
|
|
57
|
+
end
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
raw_digest = Digest::SHA512.digest( @state )
|
|
80
|
-
self.update( raw_digest )
|
|
81
|
-
adjustments.compact.each do |adjust|
|
|
82
|
-
raw_digest = Digest::SHA512.digest( raw_digest + adjust )
|
|
59
|
+
# The clone of a PoolOfEntropy::CorePRNG object includes separate copy of internal state
|
|
60
|
+
# @return [PoolOfEntropy::CorePRNG]
|
|
61
|
+
def clone
|
|
62
|
+
PoolOfEntropy::CorePRNG.new(size, state, mix_block_id)
|
|
83
63
|
end
|
|
84
|
-
fold_bits( fold_bits( raw_digest ) )
|
|
85
|
-
end
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
# Mixes supplied data into the curent state. This is called
|
|
66
|
+
# internally by #read_... methods as well.
|
|
67
|
+
# @param [String] data Data to be mixed. Note empty string '' and nil are equivalent and *do* change the state.
|
|
68
|
+
# @return [nil]
|
|
69
|
+
def update(data)
|
|
70
|
+
new_block = Digest::SHA512.digest(@state + data.to_s)
|
|
71
|
+
@state[64 * @mix_block_id, 64] = new_block
|
|
72
|
+
@mix_block_id = (@mix_block_id + 1) % @size
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
93
75
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
76
|
+
# Statistically flat distribution of 128 bits (16 bytes)
|
|
77
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect
|
|
78
|
+
# the return value, but not internal state
|
|
79
|
+
# @return [String] 16 characters in ASCII-8BIT encoding
|
|
80
|
+
def read_bytes(*adjustments)
|
|
81
|
+
raw_digest = Digest::SHA512.digest(@state)
|
|
82
|
+
update(raw_digest)
|
|
83
|
+
adjustments.compact.each do |adjust|
|
|
84
|
+
raw_digest = Digest::SHA512.digest(raw_digest + adjust)
|
|
85
|
+
end
|
|
86
|
+
fold_bits(fold_bits(raw_digest))
|
|
87
|
+
end
|
|
101
88
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
# Statistically flat distribution of 32 hex digits
|
|
90
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect
|
|
91
|
+
# the return value, but not internal state
|
|
92
|
+
# @return [String] 32 hex digits
|
|
93
|
+
def read_hex(*adjustments)
|
|
94
|
+
read_bytes(*adjustments).unpack1('H*')
|
|
95
|
+
end
|
|
109
96
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def generate_integer top, *adjustments
|
|
118
|
-
power = 1
|
|
119
|
-
sum = 0
|
|
120
|
-
words = []
|
|
121
|
-
|
|
122
|
-
loop do
|
|
123
|
-
words = read_bytes( *adjustments ).unpack('L>*') if words.empty?
|
|
124
|
-
sum = 2**32 * sum + words.shift
|
|
125
|
-
power *= 2**32
|
|
126
|
-
lower_bound = sum * top / power
|
|
127
|
-
break lower_bound if lower_bound == ( (sum + 1) * top ) / power
|
|
97
|
+
# Statistically flat distribution from range 0...2**128
|
|
98
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect
|
|
99
|
+
# the return value, but not internal state
|
|
100
|
+
# @return [Bignum,Fixnum] between 0 and 0xffffffffffffffffffffffffffffffff
|
|
101
|
+
def read_bignum(*adjustments)
|
|
102
|
+
nums = read_bytes(*adjustments).unpack('Q>*')
|
|
103
|
+
nums.inject(0) { |sum, v| (sum << 64) + v }
|
|
128
104
|
end
|
|
129
|
-
end
|
|
130
105
|
|
|
131
|
-
|
|
106
|
+
# Statistically flat distribution from interval 0.0...1.0, with 53-bit precision
|
|
107
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect
|
|
108
|
+
# the return value, but not internal state
|
|
109
|
+
# @return [Float] between 0.0 and 0.9999999999999999
|
|
110
|
+
def read_float(*adjustments)
|
|
111
|
+
num = read_bytes(*adjustments).unpack1('Q>*') >> 11
|
|
112
|
+
num.to_f / (2**53)
|
|
113
|
+
end
|
|
132
114
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
# Statistically flat distribution from range (0...top).
|
|
116
|
+
# If necessary, it will read more data to ensure absolute fairness. This method
|
|
117
|
+
# can generate an unbiased distribution of Bignums up to roughly half the maximum bit size
|
|
118
|
+
# allowed by Ruby (i.e. much larger than 2**128 generated in a single read)
|
|
119
|
+
# @param [Fixnum,Bignum] top upper bound of distribution, not inclusive
|
|
120
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect
|
|
121
|
+
# the return value, but not internal state
|
|
122
|
+
# @return [Fixnum,Bignum] between 0 and top-1 inclusive
|
|
123
|
+
def generate_integer(top, *adjustments)
|
|
124
|
+
power = 1
|
|
125
|
+
sum = 0
|
|
126
|
+
words = []
|
|
127
|
+
|
|
128
|
+
loop do
|
|
129
|
+
words = read_bytes(*adjustments).unpack('L>*') if words.empty?
|
|
130
|
+
sum = ((2**32) * sum) + words.shift
|
|
131
|
+
power *= 2**32
|
|
132
|
+
lower_bound = sum * top / power
|
|
133
|
+
break lower_bound if lower_bound == ((sum + 1) * top) / power
|
|
134
|
+
end
|
|
141
135
|
end
|
|
142
|
-
folded_32bits.pack('L>*')
|
|
143
|
-
end
|
|
144
136
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Xors first half of a message with second half. Only works for messages
|
|
140
|
+
# which are multiples of 8 bytes long.
|
|
141
|
+
# @param [String] msg bytes to fold
|
|
142
|
+
# @return [String] folded message, half the length of original
|
|
143
|
+
def fold_bits(msg)
|
|
144
|
+
l = msg.length / 2
|
|
145
|
+
folded_32bits = msg[0, l].unpack('L>*').zip(msg[l, l].unpack('L>*')).map do |x, y|
|
|
146
|
+
x ^ y
|
|
147
|
+
end
|
|
148
|
+
folded_32bits.pack('L>*')
|
|
149
149
|
end
|
|
150
|
-
size
|
|
151
|
-
end
|
|
152
150
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
raise
|
|
151
|
+
def validate_size(value)
|
|
152
|
+
size = Integer(value)
|
|
153
|
+
raise ArgumentError, "Size of pool must be in Range 1..256, got #{size}" if size < 1 || size > 256
|
|
154
|
+
|
|
155
|
+
size
|
|
156
156
|
end
|
|
157
|
-
state = initial_state.clone
|
|
158
|
-
state.force_encoding( 'BINARY' )
|
|
159
157
|
|
|
160
|
-
|
|
161
|
-
raise
|
|
158
|
+
def validate_state(initial_state, size)
|
|
159
|
+
raise TypeError, "Initial state must be a String, got #{initial_state.inspect}" unless initial_state.is_a? String
|
|
160
|
+
|
|
161
|
+
state = initial_state.clone
|
|
162
|
+
state.force_encoding('BINARY')
|
|
163
|
+
|
|
164
|
+
if state.size != size * 64
|
|
165
|
+
raise ArgumentError, "Initial state bad size - expected #{size * 64} bytes, got #{state.size} bytes"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
state
|
|
162
169
|
end
|
|
163
|
-
state
|
|
164
170
|
end
|
|
165
171
|
end
|
data/lib/pool_of_entropy.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pool_of_entropy/version'
|
|
4
|
+
require 'pool_of_entropy/core_prng'
|
|
3
5
|
|
|
4
6
|
# This class models a random number generator that can mix user input into
|
|
5
7
|
# the generation mechanism in a few different ways.
|
|
@@ -20,28 +22,23 @@ require "pool_of_entropy/core_prng"
|
|
|
20
22
|
# # E.g. => 12
|
|
21
23
|
# sleep 5
|
|
22
24
|
# end
|
|
23
|
-
#
|
|
24
|
-
|
|
25
25
|
class PoolOfEntropy
|
|
26
|
-
|
|
27
26
|
# Creates a new random number source. All parameters are optional.
|
|
28
27
|
# @param [Hash] options
|
|
29
28
|
# @option options [Integer] :size, number of 512-bit (64 byte) blocks to use as internal state, defaults to 1
|
|
30
29
|
# @option options [Boolean] :blank, if true then initial state is all zeroes, otherwise use SecureRandom
|
|
31
30
|
# @option options [Array<String>] :seeds, if provided these are sent to #add_to_pool during initialize
|
|
32
31
|
# @return [PoolOfEntropy]
|
|
33
|
-
def initialize
|
|
34
|
-
unless options.is_a? Hash
|
|
35
|
-
raise TypeError, "Expecting an options hash, got #{options.inspect}"
|
|
36
|
-
end
|
|
32
|
+
def initialize(options = {})
|
|
33
|
+
raise TypeError, "Expecting an options hash, got #{options.inspect}" unless options.is_a? Hash
|
|
37
34
|
|
|
38
|
-
size = size_from_options(
|
|
35
|
+
size = size_from_options(options)
|
|
39
36
|
|
|
40
|
-
initial_state = state_from_options(
|
|
37
|
+
initial_state = state_from_options(options, size)
|
|
41
38
|
|
|
42
|
-
@core_prng = CorePRNG.new(
|
|
39
|
+
@core_prng = CorePRNG.new(size, initial_state)
|
|
43
40
|
|
|
44
|
-
seed_from_options(
|
|
41
|
+
seed_from_options(options)
|
|
45
42
|
|
|
46
43
|
@next_modifier_queue = []
|
|
47
44
|
@fixed_modifier = nil
|
|
@@ -50,7 +47,11 @@ class PoolOfEntropy
|
|
|
50
47
|
# Cloning creates a deep copy with identical PRNG state and modifiers
|
|
51
48
|
# @return [PoolOfEntropy]
|
|
52
49
|
def clone
|
|
53
|
-
|
|
50
|
+
copy = super
|
|
51
|
+
copy.instance_variable_set(:@core_prng, @core_prng.clone)
|
|
52
|
+
copy.instance_variable_set(:@fixed_modifier, @fixed_modifier.clone) if @fixed_modifier
|
|
53
|
+
copy.instance_variable_set(:@next_modifier_queue, @next_modifier_queue.map(&:clone))
|
|
54
|
+
copy
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# Same functionality as Kernel#rand or Random#rand, but using
|
|
@@ -58,19 +59,14 @@ class PoolOfEntropy
|
|
|
58
59
|
# two modifiers that are in effect.
|
|
59
60
|
# @param [Integer,Range] max if 0 then will return a Float
|
|
60
61
|
# @return [Float,Fixnum,Bignum] type depends on value of max
|
|
61
|
-
def rand
|
|
62
|
-
if max.is_a? Range
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
def rand(max = 0)
|
|
63
|
+
return rand_from_range(max) if max.is_a? Range
|
|
64
|
+
|
|
65
|
+
effective_max = max.to_i.abs
|
|
66
|
+
if effective_max.zero?
|
|
67
|
+
generate_float
|
|
67
68
|
else
|
|
68
|
-
effective_max
|
|
69
|
-
if effective_max == 0
|
|
70
|
-
return generate_float
|
|
71
|
-
else
|
|
72
|
-
return generate_integer( effective_max )
|
|
73
|
-
end
|
|
69
|
+
generate_integer(effective_max)
|
|
74
70
|
end
|
|
75
71
|
end
|
|
76
72
|
|
|
@@ -81,13 +77,13 @@ class PoolOfEntropy
|
|
|
81
77
|
# affect the internal state of the data pool used by the generator.
|
|
82
78
|
# @param [Array<String>] modifiers
|
|
83
79
|
# @return [PoolOfEntropy] self
|
|
84
|
-
def modify_next
|
|
80
|
+
def modify_next(*modifiers)
|
|
85
81
|
modifiers.each do |modifier|
|
|
86
|
-
if modifier.nil?
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
@next_modifier_queue << if modifier.nil?
|
|
83
|
+
nil
|
|
84
|
+
else
|
|
85
|
+
Digest::SHA512.digest(modifier.to_s)
|
|
86
|
+
end
|
|
91
87
|
end
|
|
92
88
|
self
|
|
93
89
|
end
|
|
@@ -100,11 +96,9 @@ class PoolOfEntropy
|
|
|
100
96
|
# affect the internal state of the data pool used by the generator.
|
|
101
97
|
# @param [String,nil] modifier
|
|
102
98
|
# @return [PoolOfEntropy] self
|
|
103
|
-
def modify_all
|
|
99
|
+
def modify_all(modifier)
|
|
104
100
|
@fixed_modifier = modifier
|
|
105
|
-
unless @fixed_modifier.nil?
|
|
106
|
-
@fixed_modifier = Digest::SHA512.digest( @fixed_modifier.to_s )
|
|
107
|
-
end
|
|
101
|
+
@fixed_modifier = Digest::SHA512.digest(@fixed_modifier.to_s) unless @fixed_modifier.nil?
|
|
108
102
|
self
|
|
109
103
|
end
|
|
110
104
|
|
|
@@ -113,8 +107,8 @@ class PoolOfEntropy
|
|
|
113
107
|
# from #rand() and cannot be undone.
|
|
114
108
|
# @param [String] data
|
|
115
109
|
# @return [PoolOfEntropy] self
|
|
116
|
-
def add_to_pool
|
|
117
|
-
@core_prng.update(
|
|
110
|
+
def add_to_pool(data)
|
|
111
|
+
@core_prng.update(data)
|
|
118
112
|
self
|
|
119
113
|
end
|
|
120
114
|
|
|
@@ -128,45 +122,51 @@ class PoolOfEntropy
|
|
|
128
122
|
|
|
129
123
|
private
|
|
130
124
|
|
|
125
|
+
def rand_from_range(range)
|
|
126
|
+
bottom = range.first
|
|
127
|
+
top = range.last
|
|
128
|
+
return nil if top < bottom
|
|
129
|
+
|
|
130
|
+
bottom + generate_integer(top - bottom + 1)
|
|
131
|
+
end
|
|
132
|
+
|
|
131
133
|
def use_adjustments
|
|
132
|
-
[
|
|
134
|
+
[@fixed_modifier, @next_modifier_queue.shift].compact
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
def generate_float
|
|
136
|
-
@core_prng.read_float(
|
|
138
|
+
@core_prng.read_float(*use_adjustments)
|
|
137
139
|
end
|
|
138
140
|
|
|
139
|
-
def generate_integer
|
|
140
|
-
@core_prng.generate_integer(
|
|
141
|
+
def generate_integer(max)
|
|
142
|
+
@core_prng.generate_integer(max, *use_adjustments)
|
|
141
143
|
end
|
|
142
144
|
|
|
143
|
-
def state_from_options(
|
|
145
|
+
def state_from_options(options, size)
|
|
144
146
|
if options[:blank]
|
|
145
147
|
"\x0" * size * 64
|
|
146
148
|
else
|
|
147
|
-
SecureRandom.random_bytes(
|
|
149
|
+
SecureRandom.random_bytes(size * 64)
|
|
148
150
|
end
|
|
149
151
|
end
|
|
150
152
|
|
|
151
|
-
def size_from_options(
|
|
153
|
+
def size_from_options(options)
|
|
152
154
|
size = 1
|
|
153
155
|
if options[:size]
|
|
154
|
-
size = Integer(
|
|
155
|
-
if size < 1 || size > 256
|
|
156
|
-
raise ArgumentError, "Size of pool must be in Range 1..256, got #{size}"
|
|
157
|
-
end
|
|
156
|
+
size = Integer(options[:size])
|
|
157
|
+
raise ArgumentError, "Size of pool must be in Range 1..256, got #{size}" if size < 1 || size > 256
|
|
158
158
|
end
|
|
159
159
|
size
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
def seed_from_options(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
def seed_from_options(options)
|
|
163
|
+
return unless options[:seeds]
|
|
164
|
+
unless options[:seeds].is_a? Array
|
|
165
|
+
raise TypeError, "Expected value for :seeds to be an Array, got #{options[:seeds].inspect}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
options[:seeds].each do |seed|
|
|
169
|
+
add_to_pool(seed)
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
end
|