secretsharing 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,67 @@
1
+ == Description
2
+ A libary for sharing secrets in an information-theoretically secure way.
3
+ It uses Shamir's secret sharing to enable sharing a (random) secret between
4
+ n persons where k <= n persons are enough to recover the secret. k-1 secret
5
+ share holders learn nothing about the secret when they combine their shares.
6
+
7
+ This library is based on the OpenXPKI::Crypto::Secret::Split Perl module used
8
+ in the open source PKI software OpenXPKI, which was written by Alexander Klink
9
+ for the OpenXPKI project in 2006.
10
+
11
+ == Prerequisites
12
+ This package requires Ruby 1.8 or later.
13
+
14
+ == Installation instructions
15
+ rake test (optional)
16
+ rake install (non-gem) or rake install_gem (gem)
17
+
18
+ == Synopsis
19
+ require 'secretsharing'
20
+
21
+ # create an object for 3 out of 5 secret sharing
22
+ s = SecretSharing::Shamir.new(5,3)
23
+
24
+ # create a random secret (returns the secret)
25
+ s.create_random_secret()
26
+
27
+ # show secret
28
+ puts s.secret
29
+
30
+ # show shares
31
+ s.shares.each { |share| puts share }
32
+
33
+ # recover secret from shares
34
+
35
+ s2 = SecretSharing::Shamir.new(3)
36
+ # accepts SecretSharing::Shamir::Share objects or
37
+ # string representations thereof
38
+ s2 << s.shares[0]
39
+ s2 << s.shares[2]
40
+ s2 << s.shares[4]
41
+ puts s2.secret
42
+
43
+ == Future Plans
44
+ Add support for using your own instead of a random secret.
45
+
46
+ == Copyright
47
+ (c) 2010 Alexander Klink
48
+
49
+ == License
50
+ Licensed under the Apache License, Version 2.0 (the "License");
51
+ you may not use this file except in compliance with the License.
52
+ You may obtain a copy of the License at
53
+
54
+ http://www.apache.org/licenses/LICENSE-2.0
55
+
56
+ == Warranty
57
+ Unless required by applicable law or agreed to in writing, software
58
+ distributed under the License is distributed on an "AS IS" BASIS,
59
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
60
+ See the License for the specific language governing permissions and
61
+ limitations under the License.
62
+
63
+ == Author
64
+ Alexander Klink
65
+ secretsharing@alech.de
66
+ http://www.alech.de
67
+ @alech on Twitter
@@ -0,0 +1 @@
1
+ require 'secretsharing/shamir'
@@ -0,0 +1,253 @@
1
+ require 'openssl'
2
+ require 'digest/sha1'
3
+
4
+ module SecretSharing
5
+ # The SecretSharing::Shamir class can be used to share random
6
+ # secrets between n people, so that k < n people can recover the
7
+ # secret, but k-1 people learn nothing (in an information-theoretical
8
+ # sense) about the secret.
9
+ #
10
+ # For a theoretical background, see
11
+ # http://www.cs.tau.ac.il/~bchor/Shamir.html or
12
+ # http://en.wikipedia.org/wiki/Secret_sharing#Shamir.27s_scheme
13
+ #
14
+ # To share a secret, create a new SecretSharing::Shamir object and
15
+ # then call the create_random_secret() method. The secret is now in
16
+ # the secret attribute and the shares are an array in the shares attribute.
17
+ #
18
+ # To recover a secret, create a SecretSharing::Shamir object and
19
+ # add the necessary shares to it using the '<<' method. Once enough
20
+ # shares have been added, the secret can be recovered in the secret
21
+ # attribute.
22
+ class Shamir
23
+ attr_reader :n, :k, :secret, :secret_bitlength, :shares
24
+
25
+ DEFAULT_SECRET_BITLENGTH = 256
26
+
27
+ # To create a new SecretSharing::Shamir object, you can
28
+ # pass either just n, or k and n.
29
+ #
30
+ # For example:
31
+ # s = SecretSharing::Shamir.new(5, 3)
32
+ # to create an object for 3 out of 5 secret sharing.
33
+ #
34
+ # or
35
+ # s = SecretSharing::Shamir.new(3)
36
+ # for 3 out of 3 secret sharing.
37
+ def initialize(n, k=n)
38
+ if k > n then
39
+ raise ArgumentError, 'k must be smaller or equal than n'
40
+ end
41
+ if k < 2 then
42
+ raise ArgumentError, 'k must be greater or equal to two'
43
+ end
44
+ if n > 255 then
45
+ raise ArgumentError, 'n must be smaller than 256'
46
+ end
47
+ @n = n
48
+ @k = k
49
+ @secret = nil
50
+ @shares = []
51
+ @received_shares = []
52
+ end
53
+
54
+ # Check whether the secret is set.
55
+ def secret_set?
56
+ ! @secret.nil?
57
+ end
58
+
59
+ # Create a random secret of a certain bitlength. Returns the
60
+ # secret and stores it in the 'secret' attribute.
61
+ def create_random_secret(bitlength = DEFAULT_SECRET_BITLENGTH)
62
+ raise 'secret already set' if secret_set?
63
+ raise 'max bitlength is 1024' if bitlength > 1024
64
+ @secret = get_random_number(bitlength)
65
+ @secret_bitlength = bitlength
66
+ create_shares
67
+ @secret
68
+ end
69
+
70
+ # Add a secret share to the object. Accepts either a SecretSharing::Shamir::Share
71
+ # instance or a string representing one. Returns true if enough shares have
72
+ # been added to recover the secret, false otherweise.
73
+ def <<(share)
74
+ # convert from string if needed
75
+ if share.class != SecretSharing::Shamir::Share then
76
+ if share.class == String then
77
+ share = SecretSharing::Shamir::Share.from_string(share)
78
+ else
79
+ raise ArgumentError 'SecretSharing::Shamir::Share or String needed'
80
+ end
81
+ end
82
+ if @received_shares.include? share then
83
+ raise 'share has already been added'
84
+ end
85
+ if @received_shares.length == @k then
86
+ raise 'we already have enough shares, no need to add more'
87
+ end
88
+ @received_shares << share
89
+ if @received_shares.length == @k then
90
+ recover_secret
91
+ return true
92
+ end
93
+ false
94
+ end
95
+
96
+ # Computes the smallest prime of a given bitlength. Uses prime_fasttest
97
+ # from the OpenSSL library with 20 attempts to be compatible to openssl
98
+ # prime, which is used in the OpenXPKI::Crypto::Secret::Split library.
99
+ def self.smallest_prime_of_bitlength(bitlength)
100
+ # start with 2^bit_length + 1
101
+ test_prime = OpenSSL::BN.new((2**bitlength + 1).to_s)
102
+ prime_found = false
103
+ while (! prime_found) do
104
+ # prime_fasttest? 20 do be compatible to
105
+ # openssl prime, which is used in OpenXPKI::Crypto::Secret::Split
106
+ prime_found = test_prime.prime_fasttest? 20
107
+ test_prime += 2
108
+ end
109
+ test_prime
110
+ end
111
+
112
+ private
113
+ # Creates a random number of a certain bitlength, optionally ensuring the
114
+ # bitlength by setting the highest bit to 1.
115
+ def get_random_number(bitlength, highest_bit_one = true)
116
+ byte_length = (bitlength / 8.0).ceil
117
+ rand_hex = OpenSSL::Random.random_bytes(byte_length).each_byte.to_a.map { |a| "%02x" % a }.join('')
118
+ rand = OpenSSL::BN.new(rand_hex, 16)
119
+ begin
120
+ rand.mask_bits!(bitlength)
121
+ rescue OpenSSL::BNError
122
+ # never mind if there was an error, this just means
123
+ # rand was already smaller than 2^bitlength - 1
124
+ end
125
+ if highest_bit_one then
126
+ rand.set_bit!(bitlength)
127
+ end
128
+ rand
129
+ end
130
+
131
+ # Creates the shares by computing random coefficients for a polynomial
132
+ # and then computing points on this polynomial.
133
+ def create_shares
134
+ @coefficients = []
135
+ @coefficients[0] = @secret
136
+
137
+ # round up to next nibble
138
+ next_nibble_bitlength = @secret_bitlength + (4 - (@secret_bitlength % 4))
139
+ prime_bitlength = next_nibble_bitlength + 1
140
+ @prime = self.class.smallest_prime_of_bitlength(prime_bitlength)
141
+
142
+ # compute random coefficients
143
+ (1..k-1).each do |x|
144
+ @coefficients[x] = get_random_number(@secret_bitlength)
145
+ end
146
+
147
+ (1..n).each do |x|
148
+ @shares[x-1] = construct_share(x, prime_bitlength)
149
+ end
150
+ end
151
+
152
+ # Construct a share by evaluating the polynomial at x and creating
153
+ # a SecretSharing::Shamir::Share object.
154
+ def construct_share(x, bitlength)
155
+ p_x = evaluate_polynomial_at(x)
156
+ SecretSharing::Shamir::Share.new(x, p_x, @prime, bitlength)
157
+ end
158
+
159
+ # Evaluate the polynomial at x.
160
+ def evaluate_polynomial_at(x)
161
+ result = OpenSSL::BN.new('0')
162
+ @coefficients.each_with_index do |coeff, i|
163
+ result += coeff * OpenSSL::BN.new(x.to_s)**i
164
+ result %= @prime
165
+ end
166
+ result
167
+ end
168
+
169
+ # Recover the secret by doing Lagrange interpolation.
170
+ def recover_secret
171
+ @secret = OpenSSL::BN.new('0')
172
+ @received_shares.each do |share|
173
+ summand = share.y * l(share.x, @received_shares)
174
+ summand %= share.prime
175
+ @secret += summand
176
+ @secret %= share.prime
177
+ end
178
+ end
179
+
180
+ # Part of the Lagrange interpolation.
181
+ # This is l_j(0), i.e.
182
+ # \prod_{x_j \neq x_i} \frac{-x_i}{x_j - x_i}
183
+ # for more information compare Wikipedia:
184
+ # http://en.wikipedia.org/wiki/Lagrange_form
185
+ def l(x, shares)
186
+ shares.select { |s| s.x != x }.map do |s|
187
+ OpenSSL::BN.new((-s.x).to_s) *
188
+ OpenSSL::BN.new((x - s.x).to_s).mod_inverse(shares[0].prime)
189
+ end.inject { |p, f| p.mod_mul(f, shares[0].prime) }
190
+ end
191
+ end
192
+
193
+ # A SecretSharing::Shamir::Share object represents a share in the
194
+ # Shamir secret sharing scheme. The share consists of a point (x,y) on
195
+ # a polynomial over Z/Zp, where p is a prime.
196
+ class SecretSharing::Shamir::Share
197
+ attr_reader :x, :y, :prime_bitlength, :prime
198
+
199
+ FORMAT_VERSION = '0'
200
+
201
+ # Create a new share with the given point, prime and prime bitlength.
202
+ def initialize(x, y, prime, prime_bitlength)
203
+ @x = x
204
+ @y = y
205
+ @prime = prime
206
+ @prime_bitlength = prime_bitlength
207
+ end
208
+
209
+ # Create a new share from a string format representation. For
210
+ # a discussion of the format, see the to_s() method.
211
+ def self.from_string(string)
212
+ version = string[0,1]
213
+ if version != '0' then
214
+ raise "invalid share format version #{version}."
215
+ end
216
+ x = string[1,2].hex
217
+ prime_bitlength = 4 * string[-2,2].hex + 1
218
+ p_x_str = string[3, string.length - 9]
219
+ checksum = string[-6, 4]
220
+ computed_checksum = Digest::SHA1.hexdigest(p_x_str)[0,4].upcase
221
+ if checksum != computed_checksum then
222
+ raise "invalid checksum. expected #{checksum}, got #{computed_checksum}"
223
+ end
224
+ prime = SecretSharing::Shamir.smallest_prime_of_bitlength(prime_bitlength)
225
+ self.new(x, OpenSSL::BN.new(p_x_str, 16), prime, prime_bitlength)
226
+ end
227
+
228
+ # A string representation of the share, that can for example be
229
+ # distributed in printed form.
230
+ # The string is an uppercase hexadecimal string of the following
231
+ # format: ABBC*DDDDEEEE, where
232
+ # * A (the first nibble) is the version number of the format, currently
233
+ # fixed to 0.
234
+ # * B (the next byte, two hex characters) is the x coordinate of the point
235
+ # on the polynomial.
236
+ # * C (the next variable length of bytes) is the y coordinate of the point
237
+ # on the polynomial.
238
+ # * D (the next two bytes, four hex characters) is the two highest bytes of
239
+ # the SHA1 hash on the string representing the y coordinate, it is used as
240
+ # a checksum to guard against typos
241
+ # * E (the next two bytes, four hex characters) is the bitlength of the prime
242
+ # number in nibbles.
243
+ def to_s
244
+ # bitlength in nibbles to save space
245
+ prime_nibbles = (@prime_bitlength - 1) / 4
246
+ p_x = ("%x" % @y).upcase
247
+ FORMAT_VERSION + ("%02x" % @x).upcase \
248
+ + p_x \
249
+ + Digest::SHA1.hexdigest(p_x)[0,4].upcase \
250
+ + ("%02x" % prime_nibbles).upcase
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,99 @@
1
+ require 'test/unit'
2
+ require 'lib/secretsharing/shamir'
3
+
4
+ DEFAULT_SECRET_BITLENGTH = 256
5
+
6
+ class TestShamir < Test::Unit::TestCase
7
+ def test_instantiation
8
+ assert_raise( ArgumentError ) { SecretSharing::Shamir.new }
9
+ s1 = SecretSharing::Shamir.new(5)
10
+ assert_equal(5, s1.n)
11
+ assert_equal(5, s1.k)
12
+ assert(! s1.secret_set?)
13
+ s2 = SecretSharing::Shamir.new(5, 3)
14
+ assert_equal(5, s2.n)
15
+ assert_equal(3, s2.k)
16
+ assert(! s2.secret_set?)
17
+ assert_raise( ArgumentError ) { SecretSharing::Shamir.new(5, 7) }
18
+ assert_raise( ArgumentError ) { SecretSharing::Shamir.new(1) }
19
+ end
20
+
21
+ def test_create_random_secret
22
+ s = SecretSharing::Shamir.new(5)
23
+ s.create_random_secret()
24
+ assert(s.secret_set?)
25
+ assert_not_nil(s.secret)
26
+ assert_not_nil(s.shares)
27
+ assert_equal(Array, s.shares.class)
28
+ assert_equal(5, s.shares.length)
29
+ assert_equal(SecretSharing::Shamir::Share, s.shares[0].class)
30
+ assert_equal(DEFAULT_SECRET_BITLENGTH, s.secret_bitlength)
31
+
32
+ # can only be called once
33
+ assert_raise( RuntimeError) { s.create_random_secret() }
34
+
35
+ s2 = SecretSharing::Shamir.new(7)
36
+ s2.create_random_secret(512)
37
+ assert_equal(512, s2.secret_bitlength)
38
+ end
39
+
40
+ def test_recover_secret_k_eq_n
41
+ s = SecretSharing::Shamir.new(5)
42
+ s.create_random_secret()
43
+
44
+ s2 = SecretSharing::Shamir.new(5)
45
+ s2 << s.shares[0]
46
+ assert(! s2.secret_set?)
47
+ assert_nil(s2.secret)
48
+ # adding the same share raises an error
49
+ assert_raise( RuntimeError ) { s2 << s.shares[0] }
50
+ # add more shares
51
+ s2 << s.shares[1]
52
+ assert(! s2.secret_set?)
53
+ s2 << s.shares[2]
54
+ assert(! s2.secret_set?)
55
+ s2 << s.shares[3]
56
+ assert(! s2.secret_set?)
57
+ s2 << s.shares[4]
58
+ assert(s2.secret_set?)
59
+ assert_equal(s.secret, s2.secret)
60
+ end
61
+
62
+ def test_recover_secret_k_le_n
63
+ s = SecretSharing::Shamir.new(5, 3)
64
+ s.create_random_secret()
65
+
66
+ s2 = SecretSharing::Shamir.new(5, 3)
67
+ s2 << s.shares[0]
68
+ assert(! s2.secret_set?)
69
+ assert_nil(s2.secret)
70
+ # add more shares
71
+ s2 << s.shares[1]
72
+ assert(! s2.secret_set?)
73
+ s2 << s.shares[2]
74
+ assert(s2.secret_set?)
75
+ assert_equal(s.secret, s2.secret)
76
+
77
+ # adding more shares than needed raises an error
78
+ assert_raise( RuntimeError ) { s2 << s.shares[3] }
79
+ end
80
+
81
+ def test_recover_secret_k_le_n_strings
82
+ s = SecretSharing::Shamir.new(5, 3)
83
+ s.create_random_secret()
84
+
85
+ s2 = SecretSharing::Shamir.new(5, 3)
86
+ s2 << "#{s.shares[0]}"
87
+ assert(! s2.secret_set?)
88
+ assert_nil(s2.secret)
89
+ # add more shares
90
+ s2 << "#{s.shares[1]}"
91
+ assert(! s2.secret_set?)
92
+ s2 << s.shares[2].to_s
93
+ assert(s2.secret_set?)
94
+ assert_equal(s.secret, s2.secret)
95
+
96
+ # adding more shares than needed raises an error
97
+ assert_raise( RuntimeError ) { s2 << s.shares[3] }
98
+ end
99
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secretsharing
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Klink
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-09-19 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: |
17
+ A libary for sharing secrets in an information-theoretically secure way.
18
+ It uses Shamir's secret sharing to enable sharing a (random) secret between
19
+ n persons where k <= n persons are enough to recover the secret. k-1 secret
20
+ share holders learn nothing about the secret when they combine their shares.
21
+
22
+ email: secretsharing@alech.de
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README
29
+ files:
30
+ - lib/secretsharing.rb
31
+ - lib/secretsharing/shamir.rb
32
+ - test/test_shamir.rb
33
+ - README
34
+ has_rdoc: true
35
+ homepage:
36
+ licenses: []
37
+
38
+ post_install_message:
39
+ rdoc_options: []
40
+
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.3.5
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: A library to share secrets in an information-theoretically secure way.
62
+ test_files:
63
+ - test/test_shamir.rb