secretsharing 0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coco.yml +6 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +29 -0
- data/.travis.yml +16 -0
- data/CHANGES +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.md +286 -0
- data/Rakefile +12 -0
- data/SIGNED.md +99 -0
- data/bin/secretsharing +111 -0
- data/gemfiles/Gemfile.ci +7 -0
- data/lib/secretsharing.rb +25 -0
- data/lib/secretsharing/shamir.rb +91 -293
- data/lib/secretsharing/shamir/container.rb +93 -0
- data/lib/secretsharing/shamir/secret.rb +123 -0
- data/lib/secretsharing/shamir/share.rb +156 -0
- data/lib/secretsharing/version.rb +20 -0
- data/secretsharing.gemspec +47 -0
- data/spec/shamir_container_spec.rb +373 -0
- data/spec/shamir_secret_spec.rb +208 -0
- data/spec/shamir_share_spec.rb +59 -0
- data/spec/shamir_spec.rb +35 -0
- data/spec/spec_helper.rb +33 -0
- metadata +204 -57
- data/README +0 -67
- data/test/test_shamir.rb +0 -161
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2011-2015 Alexander Klink and Glenn Rempe
|
4
|
+
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module SecretSharing
|
18
|
+
module Shamir
|
19
|
+
# The SecretSharing::Shamir::Container class can be used to share random
|
20
|
+
# secrets between n people, so that k < n people can recover the
|
21
|
+
# secret, but k-1 people learn nothing (in an information-theoretical
|
22
|
+
# sense) about the secret.
|
23
|
+
#
|
24
|
+
# For a theoretical background, see:
|
25
|
+
# http://www.cs.tau.ac.il/~bchor/Shamir.html
|
26
|
+
# http://en.wikipedia.org/wiki/Secret_sharing#Shamir.27s_scheme
|
27
|
+
#
|
28
|
+
class Container
|
29
|
+
include SecretSharing::Shamir
|
30
|
+
attr_reader :n, :k, :secret, :shares
|
31
|
+
|
32
|
+
MIN_SHARES = 2
|
33
|
+
MAX_SHARES = 512
|
34
|
+
|
35
|
+
# To create a new SecretSharing::Shamir::Container object, you can
|
36
|
+
# pass either just n, or n and k where:
|
37
|
+
#
|
38
|
+
# n = The total number of shares that will be created.
|
39
|
+
# k = The threshold number of the total shares needed to
|
40
|
+
# recreate the original secret. (Default = n)
|
41
|
+
#
|
42
|
+
# For example:
|
43
|
+
#
|
44
|
+
# # 3(k) out of 5(n) shares needed to recover secret
|
45
|
+
# s = SecretSharing::Shamir::Container.new(5, 3)
|
46
|
+
#
|
47
|
+
# # 3(k) out of 3(n) shares needed to recover secret
|
48
|
+
# s = SecretSharing::Shamir::Container.new(3)
|
49
|
+
#
|
50
|
+
def initialize(n, k = n)
|
51
|
+
@n = n.to_i
|
52
|
+
@k = k.to_i
|
53
|
+
|
54
|
+
fail ArgumentError, 'n must be an Integer' unless @n.is_a?(Integer)
|
55
|
+
fail ArgumentError, 'k must be an Integer' unless @k.is_a?(Integer)
|
56
|
+
|
57
|
+
fail ArgumentError, 'k must be <= n' unless @k <= @n
|
58
|
+
fail ArgumentError, 'k must be >= #{MIN_SHARES}' unless @k >= MIN_SHARES
|
59
|
+
fail ArgumentError, 'n must be <= #{MAX_SHARES}' unless @n <= MAX_SHARES
|
60
|
+
|
61
|
+
@secret = nil
|
62
|
+
@shares = []
|
63
|
+
end
|
64
|
+
|
65
|
+
def secret?
|
66
|
+
@secret.is_a?(SecretSharing::Shamir::Secret)
|
67
|
+
end
|
68
|
+
|
69
|
+
def secret=(sec)
|
70
|
+
fail ArgumentError, 'secret has already been set' if secret?
|
71
|
+
fail ArgumentError, 'secret must be a SecretSharing::Shamir::Secret instance' unless sec.is_a?(SecretSharing::Shamir::Secret)
|
72
|
+
@secret = sec
|
73
|
+
@shares = Share.create_shares(@k, @n, @secret)
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Add a secret share to the object. Accepts a
|
78
|
+
# SecretSharing::Shamir::Share instance.
|
79
|
+
# Returns secret as a SecretSharing::Shamir::Secret if enough valid shares have been added
|
80
|
+
# to recover the secret, and false otherwise. The secret can also be recovered
|
81
|
+
# later with SecretSharing::Shamir::Container#secret if enough valid shares were previously
|
82
|
+
# provided.
|
83
|
+
def <<(share)
|
84
|
+
# You can't add more shares than were originally generated with value of @n
|
85
|
+
fail ArgumentError, 'You have added more shares than allowed by the value of @n' if @shares.size >= @n
|
86
|
+
|
87
|
+
share = SecretSharing::Shamir::Share.new(:share => share) unless share.is_a?(SecretSharing::Shamir::Share)
|
88
|
+
@shares << share unless @shares.include?(share)
|
89
|
+
@secret = Share.recover_secret(@shares)
|
90
|
+
end
|
91
|
+
end # class Container
|
92
|
+
end # module Shamir
|
93
|
+
end # module SecretSharing
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2011-2015 Glenn Rempe
|
4
|
+
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module SecretSharing
|
18
|
+
module Shamir
|
19
|
+
# A SecretSharing::Shamir::Secret object represents a Secret in the
|
20
|
+
# Shamir secret sharing scheme. Secrets can be passed in as an input
|
21
|
+
# argument when creating a new SecretSharing::Shamir::Container or
|
22
|
+
# can be the output from a Container that has successfully decoded shares.
|
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
|
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.
|
30
|
+
#
|
31
|
+
# All secrets are internally represented as an OpenSSL::BN which can be retrieved
|
32
|
+
# in its raw form using #secret.
|
33
|
+
#
|
34
|
+
class Secret
|
35
|
+
include SecretSharing::Shamir
|
36
|
+
|
37
|
+
# FIXME : Is a MAX_BITLENGTH really needed? Can it be larger if so?
|
38
|
+
|
39
|
+
MAX_BITLENGTH = 4096
|
40
|
+
|
41
|
+
attr_accessor :secret, :bitlength, :hmac
|
42
|
+
|
43
|
+
# FIXME : allow instantiating a secret with any random number bitlength you choose.
|
44
|
+
|
45
|
+
def initialize(opts = {})
|
46
|
+
opts = {
|
47
|
+
:secret => get_random_number(256)
|
48
|
+
}.merge!(opts)
|
49
|
+
|
50
|
+
# override with options
|
51
|
+
opts.each_key do |k|
|
52
|
+
if self.respond_to?("#{k}=")
|
53
|
+
send("#{k}=", opts[k])
|
54
|
+
else
|
55
|
+
fail ArgumentError, "Argument '#{k}' is not allowed"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# 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
|
+
|
62
|
+
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
|
+
# 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)
|
68
|
+
end
|
69
|
+
|
70
|
+
@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
|
73
|
+
fail ArgumentError, "Secret must have a bitlength less than or equal to #{MAX_BITLENGTH}" if @bitlength > MAX_BITLENGTH
|
74
|
+
|
75
|
+
generate_hmac
|
76
|
+
end
|
77
|
+
|
78
|
+
# Secrets are equal if the OpenSSL::BN in @secret is the same.
|
79
|
+
def ==(other)
|
80
|
+
other == @secret
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set a new secret forces regeneration of the HMAC
|
84
|
+
def secret=(secret)
|
85
|
+
@secret = secret
|
86
|
+
generate_hmac
|
87
|
+
end
|
88
|
+
|
89
|
+
def secret?
|
90
|
+
@secret.is_a?(OpenSSL::BN)
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
# Convert the OpenSSL::BN secret to an Bignum which has a #to_s(36) method
|
95
|
+
# Convert the Bignum to a Base 36 encoded String
|
96
|
+
# Wrap the Base 36 encoded String as a URL safe Base 64 encoded String
|
97
|
+
# Combined this should result in a relatively compact and portable String
|
98
|
+
usafe_encode64(@secret.to_i.to_s(36))
|
99
|
+
end
|
100
|
+
|
101
|
+
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)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
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.
|
115
|
+
def generate_hmac
|
116
|
+
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)
|
120
|
+
end
|
121
|
+
end # class Secret
|
122
|
+
end # module Shamir
|
123
|
+
end # module SecretSharing
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2011-2015 Alexander Klink and Glenn Rempe
|
4
|
+
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module SecretSharing
|
18
|
+
module Shamir
|
19
|
+
# A SecretSharing::Shamir::Share object represents a share in the
|
20
|
+
# Shamir secret sharing scheme. The share consists of a point (x,y) on
|
21
|
+
# a polynomial over Z/Zp, where p is a prime.
|
22
|
+
class Share
|
23
|
+
include SecretSharing::Shamir
|
24
|
+
extend SecretSharing::Shamir
|
25
|
+
attr_accessor :share, :version, :hmac, :k, :n, :x, :y, :prime, :prime_bitlength
|
26
|
+
|
27
|
+
def initialize(opts = {})
|
28
|
+
opts = {
|
29
|
+
:share => nil,
|
30
|
+
:version => 1,
|
31
|
+
:hmac => nil,
|
32
|
+
:k => nil,
|
33
|
+
:n => nil,
|
34
|
+
:x => nil,
|
35
|
+
:y => nil,
|
36
|
+
:prime => nil,
|
37
|
+
:prime_bitlength => nil
|
38
|
+
}.merge!(opts)
|
39
|
+
|
40
|
+
opts.each_key do |k|
|
41
|
+
if self.respond_to?("#{k}=")
|
42
|
+
send("#{k}=", opts[k])
|
43
|
+
else
|
44
|
+
fail ArgumentError, "Argument '#{k}' is not allowed"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Decode and unpack a String share if provided
|
49
|
+
unpack_share(@share) unless @share.nil?
|
50
|
+
|
51
|
+
if @share.nil?
|
52
|
+
errors = [:version, :hmac, :k, :n, :x, :y, :prime, :prime_bitlength].map { |e| e if send("#{e}").nil? }.compact
|
53
|
+
fail ArgumentError, "#{errors.join(', ')} expected." unless errors.empty?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
other.to_s == to_s
|
59
|
+
end
|
60
|
+
|
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
|
+
def to_hash
|
65
|
+
[:version, :hmac, :k, :n, :x, :y, :prime, :prime_bitlength].reduce({}) do |h, element|
|
66
|
+
if [:hmac].include?(element)
|
67
|
+
h.merge(element => send(element))
|
68
|
+
else
|
69
|
+
# everything else is an Integer/Bignum
|
70
|
+
h.merge(element => send(element).to_i)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_json
|
76
|
+
MultiJson.dump(to_hash)
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
usafe_encode64(to_json)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates the shares by computing random coefficients for a polynomial
|
84
|
+
# and then computing points on this polynomial.
|
85
|
+
def self.create_shares(k, n, secret)
|
86
|
+
shares = []
|
87
|
+
coefficients = []
|
88
|
+
coefficients[0] = secret.secret
|
89
|
+
|
90
|
+
# round up to next nibble
|
91
|
+
next_nibble_bitlength = secret.bitlength + (4 - (secret.bitlength % 4))
|
92
|
+
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) }
|
105
|
+
|
106
|
+
(1..n).each do |x|
|
107
|
+
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)
|
109
|
+
shares[x - 1] = new_share
|
110
|
+
end
|
111
|
+
shares
|
112
|
+
end
|
113
|
+
|
114
|
+
# Recover the secret by doing Lagrange interpolation.
|
115
|
+
def self.recover_secret(shares)
|
116
|
+
return false unless shares.length >= shares[0].k
|
117
|
+
|
118
|
+
# All Shares must have the same HMAC or they were derived from different Secrets
|
119
|
+
hmacs = shares.map(&:hmac).uniq
|
120
|
+
fail ArgumentError, 'Share mismatch. Not all Shares have a common HMAC.' unless hmacs.size == 1
|
121
|
+
|
122
|
+
secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN.new('0'))
|
123
|
+
|
124
|
+
shares.each do |share|
|
125
|
+
l_x = lagrange(share.x, shares)
|
126
|
+
summand = share.y * l_x
|
127
|
+
summand %= share.prime
|
128
|
+
secret.secret += summand
|
129
|
+
secret.secret %= share.prime
|
130
|
+
end
|
131
|
+
|
132
|
+
if secret && secret.is_a?(SecretSharing::Shamir::Secret) && secret.valid_hmac?
|
133
|
+
return secret
|
134
|
+
else
|
135
|
+
fail ArgumentError, 'Secret recovery failure. The generated Secret does not match the HMACs in the Shares provided.'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def unpack_share(share)
|
142
|
+
decoded = usafe_decode64(share)
|
143
|
+
h = MultiJson.load(decoded, :symbolize_keys => true)
|
144
|
+
|
145
|
+
@version = h[:version].to_i unless h[:version].nil?
|
146
|
+
@hmac = h[:hmac] unless h[:hmac].nil?
|
147
|
+
@k = h[:k].to_i unless h[:k].nil?
|
148
|
+
@n = h[:n].to_i unless h[:n].nil?
|
149
|
+
@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?
|
152
|
+
@prime_bitlength = h[:prime_bitlength].to_i unless h[:prime_bitlength].nil?
|
153
|
+
end
|
154
|
+
end # class Share
|
155
|
+
end # module Shamir
|
156
|
+
end # module SecretSharing
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2011-2015 Glenn Rempe
|
4
|
+
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
# Module for specifying the current version of the gem.
|
18
|
+
module SecretSharing
|
19
|
+
VERSION = '1.0.0'
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'secretsharing/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "secretsharing"
|
9
|
+
s.version = SecretSharing::VERSION
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.authors = ["Alexander Klink", "Glenn Rempe"]
|
12
|
+
s.email = ["glenn@rempe.us"]
|
13
|
+
s.homepage = "https://github.com/grempe/secretsharing"
|
14
|
+
s.summary = %q{A Ruby Gem to enable sharing secrets using Shamirs Secret Sharing.}
|
15
|
+
s.license = "APACHE 2.0"
|
16
|
+
|
17
|
+
s.has_rdoc = 'true'
|
18
|
+
s.extra_rdoc_files = ['README.md']
|
19
|
+
|
20
|
+
s.rubyforge_project = "secretsharing"
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split($/)
|
23
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
24
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
|
27
|
+
s.add_dependency 'highline', '~> 1.6'
|
28
|
+
s.add_dependency 'multi_json', '~> 1.10'
|
29
|
+
|
30
|
+
s.add_development_dependency 'mocha'
|
31
|
+
s.add_development_dependency 'minitest'
|
32
|
+
s.add_development_dependency 'coco'
|
33
|
+
s.add_development_dependency 'rb-fsevent'
|
34
|
+
s.add_development_dependency 'rerun'
|
35
|
+
s.add_development_dependency 'rubocop'
|
36
|
+
s.add_development_dependency 'bundler', '~> 1.7'
|
37
|
+
s.add_development_dependency 'rake', '~> 10.4'
|
38
|
+
|
39
|
+
s.description =<<'XEOF'
|
40
|
+
Shamir's Secret Sharing is an algorithm in cryptography. It is a
|
41
|
+
form of secret sharing, where a secret is divided into parts,
|
42
|
+
giving each participant its own unique part, where some of the
|
43
|
+
parts or all of them are needed in order to reconstruct the
|
44
|
+
secret. Holders of a share gain no knowledge of the larger secret.
|
45
|
+
XEOF
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,373 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2011-2015 Glenn Rempe
|
4
|
+
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require File.expand_path('../spec_helper', __FILE__)
|
18
|
+
|
19
|
+
describe SecretSharing::Shamir::Container do
|
20
|
+
|
21
|
+
describe 'initialization' do
|
22
|
+
|
23
|
+
it 'will raise when instantiated with no args' do
|
24
|
+
lambda { SecretSharing::Shamir::Container.new }.must_raise(ArgumentError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'will create shares with n and k equal when given one Integer arg' do
|
28
|
+
s1 = SecretSharing::Shamir::Container.new(5)
|
29
|
+
s1.n.must_equal(5)
|
30
|
+
s1.k.must_equal(5)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'will create shares with n and k set to their own values when given two Integer args' do
|
34
|
+
s1 = SecretSharing::Shamir::Container.new(5, 3)
|
35
|
+
s1.n.must_equal(5)
|
36
|
+
s1.k.must_equal(3)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'will create shares with n and k equal when given one Integer as String arg' do
|
40
|
+
s1 = SecretSharing::Shamir::Container.new('5')
|
41
|
+
s1.n.must_equal(5)
|
42
|
+
s1.k.must_equal(5)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'will raise an exception with n being a non-Integer String arg' do
|
46
|
+
lambda { SecretSharing::Shamir::Container.new('foo') }.must_raise(ArgumentError)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'will create shares with n and k set to their own values when given two Integer as String args' do
|
50
|
+
s1 = SecretSharing::Shamir::Container.new('5', '3')
|
51
|
+
s1.n.must_equal(5)
|
52
|
+
s1.k.must_equal(3)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'will return false when secret? is called after initialization with only n arg set' do
|
56
|
+
s1 = SecretSharing::Shamir::Container.new(5)
|
57
|
+
s1.secret?.must_equal(false)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'will return false when secret? is called after initialization with n and k arg set' do
|
61
|
+
s1 = SecretSharing::Shamir::Container.new(5, 3)
|
62
|
+
s1.secret?.must_equal(false)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'will return nil secret when called after initialization with only n arg set' do
|
66
|
+
s1 = SecretSharing::Shamir::Container.new(5)
|
67
|
+
s1.secret.must_be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'will return nil secret when called after initialization with n and k arg set' do
|
71
|
+
s1 = SecretSharing::Shamir::Container.new(5, 3)
|
72
|
+
s1.secret.must_be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'will raise if k > n' do
|
76
|
+
lambda { SecretSharing::Shamir::Container.new(5, 6) }.must_raise(ArgumentError)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'will raise if only n is provided and it is < 2' do
|
80
|
+
lambda { SecretSharing::Shamir::Container.new(1) }.must_raise(ArgumentError)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'will raise unless k >= 2' do
|
84
|
+
lambda { SecretSharing::Shamir::Container.new(1, 1) }.must_raise(ArgumentError)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'will initialize if both k and n are at max size of 512' do
|
88
|
+
s1 = SecretSharing::Shamir::Container.new(512, 512)
|
89
|
+
s1.n.must_equal(512)
|
90
|
+
s1.k.must_equal(512)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'will raise if n > 512' do
|
94
|
+
lambda { SecretSharing::Shamir::Container.new(513) }.must_raise(ArgumentError)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'must return the correct min shares constant value' do
|
98
|
+
SecretSharing::Shamir::Container::MIN_SHARES.must_equal(2)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'must return the correct max shares constant value' do
|
102
|
+
SecretSharing::Shamir::Container::MAX_SHARES.must_equal(512)
|
103
|
+
end
|
104
|
+
|
105
|
+
end # describe initialization
|
106
|
+
|
107
|
+
describe 'creating a container and setting a secret' do
|
108
|
+
|
109
|
+
before do
|
110
|
+
@num_shares = 5
|
111
|
+
@c = SecretSharing::Shamir::Container.new(@num_shares)
|
112
|
+
@secret_num = OpenSSL::BN.new('1234567890')
|
113
|
+
@c.secret = SecretSharing::Shamir::Secret.new(:secret => @secret_num)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'will return true from #secret?' do
|
117
|
+
@c.secret?.must_equal(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'will not return a nil #secret' do
|
121
|
+
@c.secret.wont_be_nil
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'will not return a nil #shares' do
|
125
|
+
@c.shares.wont_be_nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'will return an Array of #shares' do
|
129
|
+
@c.shares.must_be_instance_of(Array)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'will return an Array of #shares of the same length as initialized with' do
|
133
|
+
@c.shares.size.must_equal(@num_shares)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'will return an Array of #shares each of the correct class' do
|
137
|
+
@c.shares.each do |share|
|
138
|
+
share.must_be_instance_of(SecretSharing::Shamir::Share)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'must raise an exception if a secret is attempted to be set more than once' do
|
143
|
+
lambda { @c.secret = SecretSharing::Shamir::Secret.new }.must_raise(ArgumentError)
|
144
|
+
end
|
145
|
+
|
146
|
+
end # creating a container and setting a secret
|
147
|
+
|
148
|
+
describe 'generating shares when a secret is provided' do
|
149
|
+
|
150
|
+
it 'should generate unique shares for the min number of shares and a tiny secret' do
|
151
|
+
c1 = SecretSharing::Shamir::Container.new(2)
|
152
|
+
c1.secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN.new('123'))
|
153
|
+
shares = c1.shares
|
154
|
+
shares.size.must_equal(2)
|
155
|
+
uniq_shares = shares.uniq
|
156
|
+
shares.must_equal(uniq_shares)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should generate unique shares for the max number of shares and a tiny secret' do
|
160
|
+
c1 = SecretSharing::Shamir::Container.new(512)
|
161
|
+
c1.secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN.new('123'))
|
162
|
+
shares = c1.shares
|
163
|
+
shares.size.must_equal(512)
|
164
|
+
uniq_shares = shares.uniq
|
165
|
+
shares.must_equal(uniq_shares)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should generate unique shares for the min number of shares and a large secret' do
|
169
|
+
c1 = SecretSharing::Shamir::Container.new(2)
|
170
|
+
# FIXME : Major Perf Issue : If OpenSSL::BN::rand(512) is given with 4096 instead of 512 this takes FOREVER
|
171
|
+
c1.secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN::rand(512))
|
172
|
+
shares = c1.shares
|
173
|
+
shares.size.must_equal(2)
|
174
|
+
uniq_shares = shares.uniq
|
175
|
+
shares.must_equal(uniq_shares)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should generate unique shares for the max number of shares and a large secret' do
|
179
|
+
c1 = SecretSharing::Shamir::Container.new(512)
|
180
|
+
# FIXME : Major Perf Issue : If OpenSSL::BN::rand(512) is given with 4096 instead of 512 this takes FOREVER
|
181
|
+
c1.secret = SecretSharing::Shamir::Secret.new(:secret => OpenSSL::BN::rand(512))
|
182
|
+
shares = c1.shares
|
183
|
+
shares.size.must_equal(512)
|
184
|
+
uniq_shares = shares.uniq
|
185
|
+
shares.must_equal(uniq_shares)
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
describe 'recovering a secret from a container' do
|
191
|
+
|
192
|
+
before do
|
193
|
+
@c1 = SecretSharing::Shamir::Container.new(5) # creator
|
194
|
+
@c2 = SecretSharing::Shamir::Container.new(5) # recipient
|
195
|
+
|
196
|
+
@c3 = SecretSharing::Shamir::Container.new(5, 3) # creator
|
197
|
+
@c4 = SecretSharing::Shamir::Container.new(5, 3) # recipient
|
198
|
+
|
199
|
+
@bad = SecretSharing::Shamir::Container.new(5, 3) # a bad actor
|
200
|
+
end
|
201
|
+
|
202
|
+
describe 'with a mix of valid and invalid shares' do
|
203
|
+
|
204
|
+
before do
|
205
|
+
# set a secret on the 'creators'
|
206
|
+
@c1.secret = SecretSharing::Shamir::Secret.new
|
207
|
+
@c3.secret = SecretSharing::Shamir::Secret.new
|
208
|
+
@bad.secret = SecretSharing::Shamir::Secret.new
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'should be able to recover secret when k equals n and all k valid shares are provided as Shamir::Share objects' do
|
212
|
+
@c2 << @c1.shares[0]
|
213
|
+
@c2 << @c1.shares[1]
|
214
|
+
@c2 << @c1.shares[2]
|
215
|
+
@c2 << @c1.shares[3]
|
216
|
+
|
217
|
+
# with the last remaining share missing
|
218
|
+
@c2.secret?.must_equal(false)
|
219
|
+
|
220
|
+
@c2 << @c1.shares[4]
|
221
|
+
|
222
|
+
# with the final share provided
|
223
|
+
@c2.secret?.must_equal(true)
|
224
|
+
@c2.secret.must_equal(@c1.secret)
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should raise an ArgumentError if n + 1 shares are provided (more than were originally generated)' do
|
228
|
+
@c2 << @c1.shares[0]
|
229
|
+
@c2 << @c1.shares[1]
|
230
|
+
@c2 << @c1.shares[2]
|
231
|
+
@c2 << @c1.shares[3]
|
232
|
+
@c2.secret?.must_equal(false)
|
233
|
+
@c2 << @c1.shares[4]
|
234
|
+
@c2.secret?.must_equal(true)
|
235
|
+
|
236
|
+
lambda { @c2 << @bad.shares[0] }.must_raise(ArgumentError)
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'should raise an ArgumentError when k equals n and k-1 valid shares and 1 invalid share are provided as Shamir::Share objects' do
|
240
|
+
@c2 << @c1.shares[0]
|
241
|
+
@c2 << @c1.shares[1]
|
242
|
+
@c2 << @c1.shares[2]
|
243
|
+
@c2 << @c1.shares[3]
|
244
|
+
|
245
|
+
# with the last remaining share missing
|
246
|
+
@c2.secret?.must_equal(false)
|
247
|
+
|
248
|
+
# with a bad share
|
249
|
+
lambda { @c2 << @bad.shares[0] }.must_raise(ArgumentError)
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'should raise an ArgumentError if the final @secret generated does not have a valid_hmac?' do
|
253
|
+
# mocha instance mock using handy 'any_instance'
|
254
|
+
SecretSharing::Shamir::Secret.any_instance.stubs(:valid_hmac?).returns(false)
|
255
|
+
@c2 << @c1.shares[0]
|
256
|
+
@c2 << @c1.shares[1]
|
257
|
+
@c2 << @c1.shares[2]
|
258
|
+
@c2 << @c1.shares[3]
|
259
|
+
lambda { @c2 << @c1.shares[4] }.must_raise(ArgumentError)
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
describe 'with valid shares resulting from a random secret' do
|
265
|
+
|
266
|
+
before do
|
267
|
+
extend SecretSharing::Shamir
|
268
|
+
# set a secret on both of the 'creators'
|
269
|
+
@c1.secret = SecretSharing::Shamir::Secret.new
|
270
|
+
@c3.secret = SecretSharing::Shamir::Secret.new
|
271
|
+
end
|
272
|
+
|
273
|
+
# This exposed a bug in the prime number generation; for instance 4*8
|
274
|
+
# passes.
|
275
|
+
it 'should be able to recover secret with a bitlength of 5*8' do
|
276
|
+
secret = SecretSharing::Shamir::Secret.new(:secret => get_random_number(5*8))
|
277
|
+
c = SecretSharing::Shamir::Container.new(4,2)
|
278
|
+
c.secret = secret
|
279
|
+
|
280
|
+
(0...4).to_a.permutation(2).collect{|e| e.sort}.uniq.each do |indexes|
|
281
|
+
c2 = SecretSharing::Shamir::Container.new(2)
|
282
|
+
indexes.each do |index|
|
283
|
+
c2 << c.shares[index]
|
284
|
+
end
|
285
|
+
c2.secret?.must_equal(true)
|
286
|
+
c2.secret.must_equal(c.secret)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'should be able to recover secret when k equals n and all k shares are provided as Shamir::Share objects' do
|
291
|
+
@c2 << @c1.shares[0]
|
292
|
+
@c2 << @c1.shares[1]
|
293
|
+
@c2 << @c1.shares[2]
|
294
|
+
@c2 << @c1.shares[3]
|
295
|
+
|
296
|
+
# with the last remaining share missing
|
297
|
+
@c2.secret?.must_equal(false)
|
298
|
+
|
299
|
+
@c2 << @c1.shares[4]
|
300
|
+
|
301
|
+
# with the final share provided
|
302
|
+
@c2.secret?.must_equal(true)
|
303
|
+
@c2.secret.must_equal(@c1.secret)
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'should be able to recover secret when k equals n and all k shares are provided as Shamir::Share objects converted to Strings' do
|
307
|
+
@c2 << @c1.shares[0].to_s
|
308
|
+
@c2 << @c1.shares[1].to_s
|
309
|
+
@c2 << @c1.shares[2].to_s
|
310
|
+
@c2 << @c1.shares[3].to_s
|
311
|
+
|
312
|
+
# with the last remaining share missing
|
313
|
+
@c2.secret?.must_equal(false)
|
314
|
+
|
315
|
+
@c2 << @c1.shares[4].to_s
|
316
|
+
|
317
|
+
# with the final share provided
|
318
|
+
@c2.secret?.must_equal(true)
|
319
|
+
@c2.secret.must_equal(@c1.secret)
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'should be able to recover secret when k < n and minimum k shares are provided as Shamir::Share objects' do
|
323
|
+
@c4 << @c3.shares[0]
|
324
|
+
@c4 << @c3.shares[1]
|
325
|
+
|
326
|
+
# with the last remaining share missing
|
327
|
+
@c4.secret?.must_equal(false)
|
328
|
+
|
329
|
+
@c4 << @c3.shares[2]
|
330
|
+
|
331
|
+
# with the final share provided
|
332
|
+
@c4.secret?.must_equal(true)
|
333
|
+
@c4.secret.must_equal(@c3.secret)
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'should be able to recover secret when k < n and minimum k shares are provided as Shamir::Share objects converted to Strings' do
|
337
|
+
@c4 << @c3.shares[0].to_s
|
338
|
+
@c4 << @c3.shares[1].to_s
|
339
|
+
|
340
|
+
# with the last remaining share missing
|
341
|
+
@c4.secret?.must_equal(false)
|
342
|
+
|
343
|
+
@c4 << @c3.shares[2].to_s
|
344
|
+
|
345
|
+
# with the final share provided
|
346
|
+
@c4.secret?.must_equal(true)
|
347
|
+
@c4.secret.must_equal(@c3.secret)
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'should be able to recover secret when k < n and more than minimum k shares are provided as Shamir::Share objects converted to Strings' do
|
351
|
+
@c4 << @c3.shares[0].to_s
|
352
|
+
@c4 << @c3.shares[1].to_s
|
353
|
+
|
354
|
+
# with the last remaining share missing
|
355
|
+
@c4.secret?.must_equal(false)
|
356
|
+
|
357
|
+
@c4 << @c3.shares[2].to_s
|
358
|
+
|
359
|
+
# with the final needed share provided
|
360
|
+
@c4.secret?.must_equal(true)
|
361
|
+
@c4.secret.must_equal(@c3.secret)
|
362
|
+
|
363
|
+
# with an extra valid share provided
|
364
|
+
@c4 << @c3.shares[3].to_s
|
365
|
+
@c4.secret?.must_equal(true)
|
366
|
+
@c4.secret.must_equal(@c3.secret)
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
|
371
|
+
end # recovering a secret from a container
|
372
|
+
|
373
|
+
end # describe SecretSharing::Shamir::Container
|