secretsharing 1.0.0 → 2.0.1

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.
@@ -5,3 +5,5 @@ gem 'rake'
5
5
  gem 'minitest'
6
6
  gem 'highline'
7
7
  gem 'mocha'
8
+ gem 'rbnacl-libsodium'
9
+ gem 'rbnacl'
@@ -14,8 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- require 'openssl'
18
- require 'digest/sha1'
17
+ require 'rbnacl/libsodium'
18
+ require 'rbnacl'
19
19
  require 'base64'
20
20
  require 'multi_json'
21
21
 
@@ -17,39 +17,127 @@
17
17
  module SecretSharing
18
18
  # Module for common methods shared across Container, Secret, or Share
19
19
  module Shamir
20
- # FIXME : Needs focused tests
21
- # Creates a random number of a certain bitlength, optionally ensuring
22
- # the bitlength by setting the highest bit to 1.
23
- def get_random_number(bitlength)
24
- byte_length = (bitlength / 8.0).ceil
25
- rand_hex = OpenSSL::Random.random_bytes(byte_length).each_byte.to_a.map { |a| sprintf('%02x', a) }.join('')
26
- rand = OpenSSL::BN.new(rand_hex, 16)
27
-
28
- begin
29
- rand.mask_bits!(bitlength)
30
- rescue OpenSSL::BNError
31
- # never mind if there was an error, this just means
32
- # rand was already smaller than 2^bitlength - 1
20
+ # Create a random number of a specified Byte length
21
+ # returns Bignum
22
+ def get_random_number(bytes)
23
+ RbNaCl::Util.bin2hex(RbNaCl::Random.random_bytes(bytes).to_s).to_i(16)
24
+ end
25
+
26
+ # Creates a random number of a exact bitlength
27
+ # returns Bignum
28
+ def get_random_number_with_bitlength(bits)
29
+ byte_length = (bits / 8.0).ceil + 10
30
+ random_num = get_random_number(byte_length)
31
+ random_num_bin_str = random_num.to_s(2) # Get 1's and 0's
32
+
33
+ # Slice off only the bits we require, convert Bits to Numeric (Bignum)
34
+ random_num_bin_str.slice(0, bits).to_i(2)
35
+ end
36
+
37
+ # Supports #miller_rabin_prime?
38
+ def mod_exp(n, e, mod)
39
+ fail ArgumentError, 'negative exponent' if e < 0
40
+ prod = 1
41
+ base = n % mod
42
+
43
+ until e.zero?
44
+ prod = (prod * base) % mod if e.odd?
45
+ e >>= 1
46
+ base = (base * base) % mod
33
47
  end
34
48
 
35
- rand.set_bit!(bitlength)
36
- rand
49
+ prod
50
+ end
51
+
52
+ # An implementation of the miller-rabin primality test.
53
+ # See : http://primes.utm.edu/prove/merged.html
54
+ # See : http://rosettacode.org/wiki/Miller-Rabin_primality_test#Ruby
55
+ # See : https://crypto.stackexchange.com/questions/71/how-can-i-generate-large-prime-numbers-for-rsa
56
+ # See : https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test
57
+ #
58
+ # Example : p primes = (3..1000).step(2).find_all {|i| miller_rabin_prime?(i,10)}
59
+ def miller_rabin_prime?(n, g = 1000)
60
+ return false if n == 1
61
+ return true if n == 2
62
+
63
+ d = n - 1
64
+ s = 0
65
+
66
+ while d.even?
67
+ d /= 2
68
+ s += 1
69
+ end
70
+
71
+ g.times do
72
+ a = 2 + rand(n - 4)
73
+ x = mod_exp(a, d, n) # x = (a**d) % n
74
+ next if x == 1 || x == n - 1
75
+ (1..s - 1).each do
76
+ x = mod_exp(x, 2, n) # x = (x**2) % n
77
+ return false if x == 1
78
+ break if x == n - 1
79
+ end
80
+ return false if x != n - 1
81
+ end
82
+
83
+ true # probably
84
+ end
85
+
86
+ # Finds a random prime number of *at least* bitlength
87
+ # Validate primeness using the miller-rabin primality test.
88
+ # Increment through odd numbers to test candidates until a good prime is found.
89
+ def get_prime_number(bitlength)
90
+ prime_cand = get_random_number_with_bitlength(bitlength + 1)
91
+ prime_cand += 1 if prime_cand.even?
92
+
93
+ # loop, adding 2 to keep it odd, until prime_cand is prime.
94
+ (prime_cand += 2) until miller_rabin_prime?(prime_cand)
95
+
96
+ prime_cand
37
97
  end
38
98
 
39
99
  # FIXME : Needs focused tests
40
100
 
41
101
  # Evaluate the polynomial at x.
42
102
  def evaluate_polynomial_at(x, coefficients, prime)
43
- result = OpenSSL::BN.new('0')
103
+ result = 0
44
104
 
45
105
  coefficients.each_with_index do |c, i|
46
- result += c * OpenSSL::BN.new(x.to_s)**i
106
+ result += c * (x**i)
47
107
  result %= prime
48
108
  end
49
109
 
50
110
  result
51
111
  end
52
112
 
113
+ def extended_gcd(a, b)
114
+ last_remainder = a.abs
115
+ remainder = b.abs
116
+ x = 0
117
+ last_x = 1
118
+ y = 1
119
+ last_y = 0
120
+
121
+ until remainder.zero?
122
+ # rubocop:disable Style/ParallelAssignment
123
+ last_remainder, (quotient, remainder) = remainder, last_remainder.divmod(remainder)
124
+ x, last_x = last_x - quotient * x, x
125
+ y, last_y = last_y - quotient * y, y
126
+ # rubocop:enable Style/ParallelAssignment
127
+ end
128
+
129
+ [last_remainder, last_x * (a < 0 ? -1 : 1)]
130
+ end
131
+
132
+ # Calculate the Modular Inverse.
133
+ # See : http://rosettacode.org/wiki/Modular_inverse#Ruby
134
+ # Based on pseudo code from http://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Iterative_method_2
135
+ def invmod(e, et)
136
+ g, x = extended_gcd(e, et)
137
+ fail ArgumentError, 'Teh maths are broken!' if g != 1
138
+ x % et
139
+ end
140
+
53
141
  # FIXME : Needs focused tests
54
142
 
55
143
  # Part of the Lagrange interpolation.
@@ -62,35 +150,14 @@ module SecretSharing
62
150
  other_shares = shares.reject { |s| s.x == x }
63
151
 
64
152
  results = other_shares.map do |s|
65
- minus_xi = OpenSSL::BN.new("#{-s.x}")
66
- one_over_xj_minus_xi = OpenSSL::BN.new("#{x - s.x}").mod_inverse(prime)
67
- minus_xi.mod_mul(one_over_xj_minus_xi, prime)
68
- end
69
-
70
- results.reduce { |a, e| a.mod_mul(e, prime) }
71
- end
72
-
73
- # FIXME : Needs focused tests
74
-
75
- # Backported for Ruby 1.8.7, REE, JRuby, Rubinious
76
- def usafe_decode64(str)
77
- str = str.strip
78
- return Base64.urlsafe_decode64(str) if Base64.respond_to?(:urlsafe_decode64)
79
-
80
- if str.include?('\n')
81
- fail(ArgumentError, 'invalid base64')
82
- else
83
- Base64.decode64(str)
153
+ minus_xi = -s.x
154
+ # was OpenSSL::BN#mod_inverse
155
+ one_over_xj_minus_xi = invmod(x - s.x, prime)
156
+ # was OpenSSL::BN#mod_mul : (self * other) % m
157
+ (minus_xi * one_over_xj_minus_xi) % prime
84
158
  end
85
- end
86
-
87
- # FIXME : Needs focused tests
88
159
 
89
- # Backported for Ruby 1.8.7, REE, JRuby, Rubinious
90
- def usafe_encode64(bin)
91
- bin = bin.strip
92
- return Base64.urlsafe_encode64(bin) if Base64.respond_to?(:urlsafe_encode64)
93
- Base64.encode64(bin).tr("\n", '')
160
+ results.reduce { |a, e| (a * e) % prime }
94
161
  end
95
162
  end # module Shamir
96
163
  end # module SecretSharing
@@ -21,14 +21,13 @@ module SecretSharing
21
21
  # argument when creating a new SecretSharing::Shamir::Container or
22
22
  # can be the output from a Container that has successfully decoded shares.
23
23
  # A new Secret take 0 or 1 args. Zero args means the Secret will be initialized
24
- # with a random OpenSSL::BN object with the Secret::DEFAULT_BITLENGTH. If a
25
- # single argument is passed it can be one of two object types, String or
26
- # OpenSSL::BN. If a String it is expected to be a specially encoded String
24
+ # with a random Numeric object with the Secret::DEFAULT_BITLENGTH. If a
25
+ # single argument is passed it can be a String, or Integer.
26
+ # If its a String, its expected to be of a special encoding
27
27
  # that was generated as the output of calling #to_s on another Secret object.
28
- # If the object type is OpenSSL::BN it can represent a number up to 4096 num_bits
29
- # in length as reported by OpenSSL::BN#num_bits.
28
+ # If the object type is an Integer it can be up to 4096 bits in length.
30
29
  #
31
- # All secrets are internally represented as an OpenSSL::BN which can be retrieved
30
+ # All secrets are internally represented as a Numeric which can be retrieved
32
31
  # in its raw form using #secret.
33
32
  #
34
33
  class Secret
@@ -38,13 +37,15 @@ module SecretSharing
38
37
 
39
38
  MAX_BITLENGTH = 4096
40
39
 
41
- attr_accessor :secret, :bitlength, :hmac
40
+ attr_accessor :bitlength, :hmac
41
+ attr_reader :secret
42
42
 
43
43
  # FIXME : allow instantiating a secret with any random number bitlength you choose.
44
44
 
45
+ # rubocop:disable Metrics/PerceivedComplexity
45
46
  def initialize(opts = {})
46
47
  opts = {
47
- :secret => get_random_number(256)
48
+ :secret => get_random_number(32) # 32 Bytes, 256 Bits
48
49
  }.merge!(opts)
49
50
 
50
51
  # override with options
@@ -57,27 +58,39 @@ module SecretSharing
57
58
  end
58
59
 
59
60
  # FIXME : Do we really need the ability for a String arg to re-instantiate a Secret?
60
- # FIXME : If its a String, shouldn't it be able to be an arbitrary String converted to/from OpenSSL::BN?
61
+ # FIXME : If its a String, shouldn't it be able to be an arbitrary String converted to/from a Number?
61
62
 
62
63
  if opts[:secret].is_a?(String)
63
- # Decode a Base64.urlsafe_encode64 String which contains a Base 36 encoded Bignum back into an OpenSSL::BN
64
+ # Decode a Base64.urlsafe_encode64 String which contains a Base 36 encoded Bignum back into a Bignum
64
65
  # See : Secret#to_s for forward encoding method.
65
- decoded_secret = usafe_decode64(opts[:secret])
66
- fail ArgumentError, 'invalid base64 (returned nil or empty String)' if decoded_secret.empty?
67
- @secret = OpenSSL::BN.new(decoded_secret.to_i(36).to_s)
66
+ stripped_secret = opts[:secret].strip
67
+ fail ArgumentError, 'invalid secret (empty String)' if stripped_secret.empty?
68
+ decoded_secret = Base64.urlsafe_decode64(stripped_secret)
69
+ fail ArgumentError, 'invalid secret (base64 decode returned nil or empty String)' if decoded_secret.empty?
70
+ int_secret = decoded_secret.to_i(36)
71
+ fail ArgumentError, 'invalid secret (not an Integer)' unless int_secret.is_a?(Integer)
72
+ fail ArgumentError, 'invalid secret (Integer bit length < 100)' unless int_secret.bit_length > 100
73
+ @secret = int_secret
68
74
  end
69
75
 
70
76
  @secret = opts[:secret] if @secret.nil?
71
- fail ArgumentError, "Secret must be an OpenSSL::BN, not a '#{@secret.class}'" unless @secret.is_a?(OpenSSL::BN)
72
- @bitlength = @secret.num_bits
77
+ fail ArgumentError, "Secret must be an Integer, not a '#{@secret.class}'" unless @secret.is_a?(Integer)
78
+
79
+ # Get the number of binary bits in this secret's value.
80
+ @bitlength = @secret.bit_length
81
+
73
82
  fail ArgumentError, "Secret must have a bitlength less than or equal to #{MAX_BITLENGTH}" if @bitlength > MAX_BITLENGTH
74
83
 
75
84
  generate_hmac
76
85
  end
86
+ # rubocop:enable Metrics/PerceivedComplexity
77
87
 
78
- # Secrets are equal if the OpenSSL::BN in @secret is the same.
88
+ # Secrets are equal if the Numeric in @secret is the same.
89
+ # Do secure constant-time comparison of the objects.
79
90
  def ==(other)
80
- other == @secret
91
+ other_secret_hash = RbNaCl::Hash.blake2b(other.secret.to_s, digest_size: 32)
92
+ own_secret_hash = RbNaCl::Hash.blake2b(@secret.to_s, digest_size: 32)
93
+ RbNaCl::Util.verify32(other_secret_hash, own_secret_hash)
81
94
  end
82
95
 
83
96
  # Set a new secret forces regeneration of the HMAC
@@ -87,36 +100,43 @@ module SecretSharing
87
100
  end
88
101
 
89
102
  def secret?
90
- @secret.is_a?(OpenSSL::BN)
103
+ @secret.is_a?(Integer)
91
104
  end
92
105
 
93
106
  def to_s
94
- # Convert the OpenSSL::BN secret to an Bignum which has a #to_s(36) method
95
107
  # Convert the Bignum to a Base 36 encoded String
96
108
  # Wrap the Base 36 encoded String as a URL safe Base 64 encoded String
97
109
  # Combined this should result in a relatively compact and portable String
98
- usafe_encode64(@secret.to_i.to_s(36))
110
+ Base64.urlsafe_encode64(@secret.to_s(36))
99
111
  end
100
112
 
113
+ # See : generate_hmac
101
114
  def valid_hmac?
102
- return false if !@secret.is_a?(OpenSSL::BN) || @hmac.to_s.empty? || @secret.to_s.empty?
103
-
104
- hmac_key = @secret.to_s
105
- hmac_data = OpenSSL::Digest::SHA256.new(@secret.to_s).hexdigest
106
-
107
- @hmac == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_key, hmac_data)
115
+ return false if !@secret.is_a?(Integer) || @hmac.to_s.empty? || @secret.to_s.empty?
116
+ hash = RbNaCl::Hash.sha512(@secret.to_s)
117
+ key = hash[0, 32]
118
+ authenticator = RbNaCl::Util.hex2bin(@hmac)
119
+ msg = hash[33, 64]
120
+ begin
121
+ RbNaCl::HMAC::SHA256.verify(key, authenticator, msg)
122
+ rescue
123
+ false
124
+ end
108
125
  end
109
126
 
110
127
  private
111
128
 
112
- # The HMAC uses the raw secret itself as the HMAC key, and the SHA256 of the secret as the data.
113
- # This allows later regeneration of the HMAC to confirm that the restored secret is in fact
114
- # identical to what was originally split into shares.
129
+ # SHA512 over @secret returns a 64 Byte array. Use the first 32 bytes
130
+ # as the HMAC key, and the last 32 bytes as the message.
131
+ #
132
+ # This will allow a point of comparison between the original secret that
133
+ # was split into shares, and the secret that was retrieved by combining shares.
115
134
  def generate_hmac
116
135
  return false if @secret.to_s.empty?
117
- hmac_key = @secret.to_s
118
- hmac_data = OpenSSL::Digest::SHA256.new(@secret.to_s).hexdigest
119
- @hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_key, hmac_data)
136
+ hash = RbNaCl::Hash.sha512(@secret.to_s)
137
+ key = hash[0, 32]
138
+ msg = hash[33, 64]
139
+ @hmac = RbNaCl::Util.bin2hex(RbNaCl::HMAC::SHA256.auth(key, msg))
120
140
  end
121
141
  end # class Secret
122
142
  end # module Shamir
@@ -54,19 +54,20 @@ module SecretSharing
54
54
  end
55
55
  end
56
56
 
57
+ # Do secure constant-time comparison of the objects.
57
58
  def ==(other)
58
- other.to_s == to_s
59
+ other_share_hash = RbNaCl::Hash.blake2b(other.to_s, digest_size: 32)
60
+ own_share_hash = RbNaCl::Hash.blake2b(to_s, digest_size: 32)
61
+ RbNaCl::Util.verify32(other_share_hash, own_share_hash)
59
62
  end
60
63
 
61
- # FIXME : Add an HMAC which 'signs' all of the attributes of the hash and which gets verified on re-hydration to make sure
62
- # that none of the attributes changed?
63
-
64
64
  def to_hash
65
65
  [:version, :hmac, :k, :n, :x, :y, :prime, :prime_bitlength].reduce({}) do |h, element|
66
66
  if [:hmac].include?(element)
67
+ # hmac value is a String
67
68
  h.merge(element => send(element))
68
69
  else
69
- # everything else is an Integer/Bignum
70
+ # everything else can be coerced to an Integer
70
71
  h.merge(element => send(element).to_i)
71
72
  end
72
73
  end
@@ -77,7 +78,7 @@ module SecretSharing
77
78
  end
78
79
 
79
80
  def to_s
80
- usafe_encode64(to_json)
81
+ Base64.urlsafe_encode64(to_json)
81
82
  end
82
83
 
83
84
  # Creates the shares by computing random coefficients for a polynomial
@@ -87,25 +88,23 @@ module SecretSharing
87
88
  coefficients = []
88
89
  coefficients[0] = secret.secret
89
90
 
90
- # round up to next nibble
91
+ # compute random coefficients
92
+ (1..k - 1).each { |x| coefficients[x] = get_random_number_with_bitlength(secret.bitlength) }
93
+
94
+ # Round up to the next nibble (half-byte)
91
95
  next_nibble_bitlength = secret.bitlength + (4 - (secret.bitlength % 4))
92
96
  prime_bitlength = next_nibble_bitlength + 1
93
- prime = OpenSSL::BN.generate_prime(prime_bitlength)
94
-
95
- # FIXME : Why does generate_prime always return 35879 for bitlength 1-15
96
- # OpenSSL::BN::generate_prime(1).to_i
97
- # => 35879
98
- # Do we need to make sure that prime_bitlength is not shorter than 64 bits?
99
- # See : https://www.mail-archive.com/openssl-dev@openssl.org/msg18835.html
100
- # See : http://ardoino.com/2005/11/maths-openssl-primes-random/
101
- # See : http://www.openssl.org/docs/apps/genrsa.html "Therefore the number of bits should not be less that 64."
102
-
103
- # compute random coefficients
104
- (1..k - 1).each { |x| coefficients[x] = get_random_number(secret.bitlength) }
97
+ prime = get_prime_number(prime_bitlength)
105
98
 
106
99
  (1..n).each do |x|
107
100
  p_x = evaluate_polynomial_at(x, coefficients, prime)
108
- new_share = new(:x => x, :y => p_x, :prime => prime, :prime_bitlength => prime_bitlength, :k => k, :n => n, :hmac => secret.hmac)
101
+ new_share = new(:x => x,
102
+ :y => p_x,
103
+ :prime => prime,
104
+ :prime_bitlength => prime_bitlength,
105
+ :k => k,
106
+ :n => n,
107
+ :hmac => secret.hmac)
109
108
  shares[x - 1] = new_share
110
109
  end
111
110
  shares
@@ -115,11 +114,13 @@ module SecretSharing
115
114
  def self.recover_secret(shares)
116
115
  return false unless shares.length >= shares[0].k
117
116
 
118
- # All Shares must have the same HMAC or they were derived from different Secrets
117
+ # All Shares must have the same HMAC if derived from same Secret
119
118
  hmacs = shares.map(&:hmac).uniq
120
- fail ArgumentError, 'Share mismatch. Not all Shares have a common HMAC.' unless hmacs.size == 1
119
+ unless hmacs.size == 1
120
+ fail ArgumentError, 'Share mismatch. Not all Shares have a common HMAC.'
121
+ end
121
122
 
122
- secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN.new('0'))
123
+ secret = SecretSharing::Shamir::Secret.new(:secret => 0)
123
124
 
124
125
  shares.each do |share|
125
126
  l_x = lagrange(share.x, shares)
@@ -139,7 +140,7 @@ module SecretSharing
139
140
  private
140
141
 
141
142
  def unpack_share(share)
142
- decoded = usafe_decode64(share)
143
+ decoded = Base64.urlsafe_decode64(share)
143
144
  h = MultiJson.load(decoded, :symbolize_keys => true)
144
145
 
145
146
  @version = h[:version].to_i unless h[:version].nil?
@@ -147,8 +148,8 @@ module SecretSharing
147
148
  @k = h[:k].to_i unless h[:k].nil?
148
149
  @n = h[:n].to_i unless h[:n].nil?
149
150
  @x = h[:x].to_i unless h[:x].nil?
150
- @y = OpenSSL::BN.new(h[:y].to_s) unless h[:y].nil?
151
- @prime = OpenSSL::BN.new(h[:prime].to_s) unless h[:prime].nil?
151
+ @y = h[:y].to_i unless h[:y].nil?
152
+ @prime = h[:prime].to_i unless h[:prime].nil?
152
153
  @prime_bitlength = h[:prime_bitlength].to_i unless h[:prime_bitlength].nil?
153
154
  end
154
155
  end # class Share