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 +67 -0
- data/lib/secretsharing.rb +1 -0
- data/lib/secretsharing/shamir.rb +253 -0
- data/test/test_shamir.rb +99 -0
- metadata +63 -0
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
|
data/test/test_shamir.rb
ADDED
@@ -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
|