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.
- 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
|