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