secretsharing 1.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +12 -9
- data/CHANGES.md +40 -0
- data/README.md +109 -77
- data/Rakefile +3 -3
- data/bin/secretsharing +7 -7
- data/gemfiles/Gemfile.ci +2 -0
- data/lib/secretsharing.rb +2 -2
- data/lib/secretsharing/shamir.rb +111 -44
- data/lib/secretsharing/shamir/secret.rb +52 -32
- data/lib/secretsharing/shamir/share.rb +27 -26
- data/lib/secretsharing/version.rb +1 -1
- data/secretsharing.gemspec +28 -21
- data/spec/shamir_container_spec.rb +13 -25
- data/spec/shamir_secret_spec.rb +34 -27
- data/spec/shamir_share_spec.rb +0 -4
- data/spec/shamir_spec.rb +108 -8
- metadata +47 -18
- data/CHANGES +0 -17
- data/SIGNED.md +0 -99
data/gemfiles/Gemfile.ci
CHANGED
data/lib/secretsharing.rb
CHANGED
data/lib/secretsharing/shamir.rb
CHANGED
@@ -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
|
-
#
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
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 =
|
103
|
+
result = 0
|
44
104
|
|
45
105
|
coefficients.each_with_index do |c, i|
|
46
|
-
result += c *
|
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 =
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
25
|
-
# single argument is passed it can be
|
26
|
-
#
|
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
|
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
|
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 :
|
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
|
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
|
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
|
-
|
66
|
-
fail ArgumentError, 'invalid
|
67
|
-
|
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
|
72
|
-
|
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
|
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
|
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?(
|
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
|
-
|
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?(
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
#
|
113
|
-
#
|
114
|
-
#
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
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
|
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
|
-
|
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
|
-
#
|
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 =
|
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,
|
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
|
117
|
+
# All Shares must have the same HMAC if derived from same Secret
|
119
118
|
hmacs = shares.map(&:hmac).uniq
|
120
|
-
|
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 =>
|
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 =
|
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 =
|
151
|
-
@prime =
|
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
|