secretsharing 1.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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