secretsharing 0.3 → 1.0.0
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 +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
|