tss 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -4
- data/.coco.yml +7 -0
- data/.editorconfig +12 -0
- data/.hound.yml +10 -0
- data/.inch.yml +9 -0
- data/.rubocop.yml +129 -40
- data/.ruby-version +1 -1
- data/.travis.yml +4 -3
- data/CHANGELOG.md +22 -0
- data/README.md +218 -162
- data/RELEASE.md +105 -0
- data/Rakefile +9 -0
- data/bin/tss +4 -1
- data/lib/tss/cli_combine.rb +136 -0
- data/lib/tss/cli_common.rb +40 -0
- data/lib/tss/cli_split.rb +156 -0
- data/lib/tss/cli_version.rb +17 -0
- data/lib/tss/combiner.rb +156 -72
- data/lib/tss/hasher.rb +4 -2
- data/lib/tss/splitter.rb +71 -33
- data/lib/tss/tss.rb +4 -5
- data/lib/tss/util.rb +4 -12
- data/lib/tss/version.rb +1 -1
- data/tss.gemspec +7 -4
- data.tar.gz.sig +0 -0
- metadata +64 -14
- metadata.gz.sig +0 -0
- data/lib/tss/cli.rb +0 -107
- data/lib/tss/types.rb +0 -4
data/lib/tss/combiner.rb
CHANGED
@@ -1,18 +1,24 @@
|
|
1
1
|
module TSS
|
2
|
-
|
2
|
+
# Combiner has responsibility for combining an Array of String shares back
|
3
|
+
# into the original secret the shares were split from. It is also responsible
|
4
|
+
# for doing extensive validation of user provided shares and ensuring
|
5
|
+
# that any recovered secret matches the hash of the original secret.
|
6
|
+
class Combiner
|
7
|
+
include Contracts::Core
|
3
8
|
include Util
|
4
9
|
|
5
|
-
|
6
|
-
constructor_type(:schema)
|
10
|
+
C = Contracts
|
7
11
|
|
8
|
-
|
9
|
-
.constrained(min_size: 1)
|
10
|
-
.constrained(max_size: 255)
|
11
|
-
.member(Types::Strict::String)
|
12
|
+
attr_reader :shares, :select_by
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
Contract ({ :shares => C::ArrayOf[String], :select_by => C::Maybe[C::Enum['first', 'sample', 'combinations']] }) => C::Any
|
15
|
+
def initialize(opts = {})
|
16
|
+
# clone the incoming shares so the object passed to this
|
17
|
+
# function doesn't get modified.
|
18
|
+
@shares = opts.fetch(:shares).clone
|
19
|
+
raise TSS::ArgumentError, 'Invalid number of shares. Must be between 1 and 255' unless @shares.size.between?(1,255)
|
20
|
+
@select_by = opts.fetch(:select_by, 'first')
|
21
|
+
end
|
16
22
|
|
17
23
|
# To reconstruct a secret from a set of shares, the following
|
18
24
|
# procedure, or any equivalent method, is used:
|
@@ -44,8 +50,9 @@ module TSS
|
|
44
50
|
# contains the octet from the ith share. The value of I(U, V) is
|
45
51
|
# computed, then appended to the output string.
|
46
52
|
#
|
47
|
-
# The output string is returned.
|
53
|
+
# The output string is returned (along with some metadata).
|
48
54
|
#
|
55
|
+
# rubocop:disable CyclomaticComplexity
|
49
56
|
def combine
|
50
57
|
# unwrap 'human' shares into binary shares
|
51
58
|
if all_shares_appear_human?(shares)
|
@@ -53,7 +60,6 @@ module TSS
|
|
53
60
|
end
|
54
61
|
|
55
62
|
validate_all_shares(shares)
|
56
|
-
orig_shares_size = shares.size
|
57
63
|
start_processing_time = Time.now
|
58
64
|
|
59
65
|
h = Util.extract_share_header(shares.sample)
|
@@ -61,23 +67,17 @@ module TSS
|
|
61
67
|
identifier = h[:identifier]
|
62
68
|
hash_id = h[:hash_id]
|
63
69
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
if
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
when 'sample'
|
71
|
-
@shares = shares.sample(threshold)
|
72
|
-
when 'combinations'
|
73
|
-
share_combinations_mode_allowed!(hash_id)
|
74
|
-
share_combinations_out_of_bounds!(shares, threshold)
|
75
|
-
end
|
70
|
+
# Select a subset of the shares provided using the chosen selection
|
71
|
+
# method. If there are exactly the right amount of shares this is a no-op.
|
72
|
+
if select_by == 'first'
|
73
|
+
@shares = shares.shift(threshold)
|
74
|
+
elsif select_by == 'sample'
|
75
|
+
@shares = shares.sample(threshold)
|
76
76
|
end
|
77
77
|
|
78
78
|
# slice out the data after the header bytes in each share
|
79
79
|
# and unpack the byte string into an Array of Byte Arrays
|
80
|
-
shares_bytes = shares.
|
80
|
+
shares_bytes = shares.map do |s|
|
81
81
|
bytestring = s.byteslice(Splitter::SHARE_HEADER_STRUCT.size..s.bytesize)
|
82
82
|
bytestring.unpack('C*') unless bytestring.nil?
|
83
83
|
end.compact
|
@@ -85,9 +85,13 @@ module TSS
|
|
85
85
|
shares_bytes_have_valid_indexes!(shares_bytes)
|
86
86
|
|
87
87
|
if select_by == 'combinations'
|
88
|
+
share_combinations_mode_allowed!(hash_id)
|
89
|
+
share_combinations_out_of_bounds!(shares, threshold)
|
90
|
+
|
88
91
|
# Build an Array of all possible `threshold` size combinations.
|
89
92
|
share_combos = shares_bytes.combination(threshold).to_a
|
90
93
|
|
94
|
+
# Try each combination until one works.
|
91
95
|
secret = nil
|
92
96
|
while secret.nil? && share_combos.present?
|
93
97
|
# Check a combination and shift it off the Array
|
@@ -99,34 +103,41 @@ module TSS
|
|
99
103
|
secret = extract_secret_from_shares!(hash_id, shares_bytes)
|
100
104
|
end
|
101
105
|
|
106
|
+
# Return a Hash with the secret and metadata
|
102
107
|
{
|
103
|
-
|
108
|
+
hash: secret[:hash],
|
109
|
+
hash_alg: secret[:hash_alg].to_s,
|
104
110
|
identifier: identifier,
|
105
|
-
|
106
|
-
|
107
|
-
processing_started_at: start_processing_time.utc.iso8601,
|
108
|
-
processing_finished_at: Time.now.utc.iso8601,
|
109
|
-
processing_time_ms: ((Time.now - start_processing_time)*1000).round(2),
|
110
|
-
secret: Util.bytes_to_utf8(secret),
|
111
|
-
shares_select_by: select_by,
|
112
|
-
combinations: share_combos.present? ? share_combos.size : nil,
|
111
|
+
process_time: ((Time.now - start_processing_time)*1000).round(2),
|
112
|
+
secret: Util.bytes_to_utf8(secret[:secret]),
|
113
113
|
threshold: threshold
|
114
114
|
}
|
115
115
|
end
|
116
|
+
# rubocop:enable CyclomaticComplexity
|
116
117
|
|
117
118
|
private
|
118
119
|
|
120
|
+
# Given a hash ID and an Array of Arrays of Share Bytes, extract a secret
|
121
|
+
# and validate it against any one-way hash that was embedded in the shares
|
122
|
+
# along with the secret.
|
123
|
+
#
|
124
|
+
# @param hash_id [Integer] the ID of the one-way hash function to test with
|
125
|
+
# @param shares_bytes [Array<Array>] the shares as Byte Arrays to be evaluated
|
126
|
+
# @return [Array<Integer>] returns the secret as an Array of Bytes if it was recovered from the shares and validated
|
127
|
+
# @raise [TSS::NoSecretError] if the secret was not able to be recovered (with no hash)
|
128
|
+
# @raise [TSS::InvalidSecretHashError] if the secret was able to be recovered but the hash test failed
|
129
|
+
Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C::Enum[:NONE, :SHA1, :SHA256] })
|
119
130
|
def extract_secret_from_shares!(hash_id, shares_bytes)
|
120
131
|
secret = []
|
121
132
|
|
122
133
|
# build up an Array of index values from each share
|
123
134
|
# u[i] equal to the first octet of the ith share
|
124
|
-
u = shares_bytes.
|
135
|
+
u = shares_bytes.map { |s| s[0] }
|
125
136
|
|
126
137
|
# loop through each byte in all the shares
|
127
138
|
# start at Array index 1 in each share's Byte Array to skip the index
|
128
139
|
(1..(shares_bytes.first.length - 1)).each do |i|
|
129
|
-
v = shares_bytes.
|
140
|
+
v = shares_bytes.map { |share| share[i] }
|
130
141
|
secret << Util.lagrange_interpolation(u, v)
|
131
142
|
end
|
132
143
|
|
@@ -139,30 +150,39 @@ module TSS
|
|
139
150
|
# RTSS : pop off the hash digest bytes from the tail of the secret. This
|
140
151
|
# leaves `secret` with only the secret bytes remaining.
|
141
152
|
orig_hash_bytes = secret.pop(Hasher.bytesize(hash_alg))
|
153
|
+
orig_hash_hex = Util.bytes_to_hex(orig_hash_bytes)
|
142
154
|
|
143
155
|
# RTSS : verify that the recombined secret computes the same hash
|
144
156
|
# digest now as when it was originally created.
|
145
157
|
new_hash_bytes = Hasher.byte_array(hash_alg, Util.bytes_to_utf8(secret))
|
158
|
+
new_hash_hex = Util.bytes_to_hex(new_hash_bytes)
|
146
159
|
|
147
|
-
|
148
|
-
return secret
|
149
|
-
else
|
160
|
+
unless Util.secure_compare(orig_hash_hex, new_hash_hex)
|
150
161
|
raise TSS::InvalidSecretHashError, 'invalid shares, hash of secret does not equal embedded hash'
|
151
162
|
end
|
163
|
+
end
|
164
|
+
|
165
|
+
if secret.present?
|
166
|
+
return { secret: secret, hash: orig_hash_hex, hash_alg: hash_alg }
|
152
167
|
else
|
153
|
-
|
154
|
-
return secret
|
155
|
-
else
|
156
|
-
raise TSS::NoSecretError, 'invalid shares, unable to recombine into a verifiable secret'
|
157
|
-
end
|
168
|
+
raise TSS::NoSecretError, 'invalid shares, unable to recombine into a verifiable secret'
|
158
169
|
end
|
159
170
|
end
|
160
171
|
|
161
|
-
#
|
172
|
+
# Strip off leading padding chars ("\u001F", decimal 31)
|
173
|
+
#
|
174
|
+
# @param secret [Array<Integer>] the secret to be stripped
|
175
|
+
# @return [Array<Integer>,nil] returns the secret, stripped of the leading padding char
|
176
|
+
Contract C::ArrayOf[C::Num] => C::Maybe[Array]
|
162
177
|
def strip_left_pad(secret)
|
163
178
|
secret.shift while secret.first == 31
|
164
179
|
end
|
165
180
|
|
181
|
+
# Do all of the shares match the pattern expected of human style shares?
|
182
|
+
#
|
183
|
+
# @param shares [Array<String>] the shares to be evaluated
|
184
|
+
# @return [true,false] returns true if all shares match the patterns, false if not
|
185
|
+
Contract C::ArrayOf[String] => C::Bool
|
166
186
|
def all_shares_appear_human?(shares)
|
167
187
|
shares.all? do |s|
|
168
188
|
# test for starting with 'tss' since regex match against
|
@@ -171,8 +191,14 @@ module TSS
|
|
171
191
|
end
|
172
192
|
end
|
173
193
|
|
194
|
+
# Convert an Array of human style shares to binary style
|
195
|
+
#
|
196
|
+
# @param shares [Array<String>] the shares to be converted
|
197
|
+
# @return [Array<String>] returns an Array of String shares in binary octet String format
|
198
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
199
|
+
Contract C::ArrayOf[String] => C::ArrayOf[String]
|
174
200
|
def convert_shares_human_to_binary(shares)
|
175
|
-
shares.
|
201
|
+
shares.map do |s|
|
176
202
|
s_b64 = s.match(Util::HUMAN_SHARE_RE)
|
177
203
|
if s_b64.present? && s_b64.to_a[1].present?
|
178
204
|
begin
|
@@ -187,60 +213,97 @@ module TSS
|
|
187
213
|
end
|
188
214
|
end
|
189
215
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
header.key?(:threshold) &&
|
197
|
-
header[:threshold].is_a?(Integer) &&
|
198
|
-
header.key?(:share_len) &&
|
199
|
-
header[:share_len].is_a?(Integer)
|
200
|
-
end
|
201
|
-
|
216
|
+
# Do all shares have a common Byte size? They are invalid if not.
|
217
|
+
#
|
218
|
+
# @param shares [Array<String>] the shares to be evaluated
|
219
|
+
# @return [true] returns true if all shares have the same Byte size
|
220
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
221
|
+
Contract C::ArrayOf[String] => C::Bool
|
202
222
|
def shares_have_same_bytesize!(shares)
|
203
223
|
shares.each do |s|
|
204
224
|
unless s.bytesize == shares.first.bytesize
|
205
225
|
raise TSS::ArgumentError, 'invalid shares, different byte lengths'
|
206
226
|
end
|
207
227
|
end
|
228
|
+
return true
|
208
229
|
end
|
209
230
|
|
231
|
+
# Do all shares have a valid header and match each other? They are invalid if not.
|
232
|
+
#
|
233
|
+
# @param shares [Array<String>] the shares to be evaluated
|
234
|
+
# @return [true] returns true if all shares have the same header
|
235
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
236
|
+
Contract C::ArrayOf[String] => C::Bool
|
210
237
|
def shares_have_valid_headers!(shares)
|
211
238
|
fh = Util.extract_share_header(shares.first)
|
239
|
+
|
240
|
+
unless Contract.valid?(fh, ({ :identifier => String, :hash_id => C::Int, :threshold => C::Int, :share_len => C::Int }))
|
241
|
+
raise TSS::ArgumentError, 'invalid shares, headers have invalid structure'
|
242
|
+
end
|
243
|
+
|
212
244
|
shares.each do |s|
|
213
|
-
|
214
|
-
|
215
|
-
raise TSS::ArgumentError, 'invalid shares, bad headers'
|
245
|
+
unless Util.extract_share_header(s) == fh
|
246
|
+
raise TSS::ArgumentError, 'invalid shares, headers do not match'
|
216
247
|
end
|
217
248
|
end
|
249
|
+
|
250
|
+
return true
|
218
251
|
end
|
219
252
|
|
253
|
+
# Do all shares have a the expected length? They are invalid if not.
|
254
|
+
#
|
255
|
+
# @param shares [Array<String>] the shares to be evaluated
|
256
|
+
# @return [true] returns true if all shares have the same header
|
257
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
258
|
+
Contract C::ArrayOf[String] => C::Bool
|
220
259
|
def shares_have_expected_length!(shares)
|
221
260
|
shares.each do |s|
|
222
261
|
unless s.bytesize > Splitter::SHARE_HEADER_STRUCT.size + 1
|
223
262
|
raise TSS::ArgumentError, 'invalid shares, too short'
|
224
263
|
end
|
225
264
|
end
|
265
|
+
return true
|
226
266
|
end
|
227
267
|
|
268
|
+
# Were enough shares provided to meet the threshold? They are invalid if not.
|
269
|
+
#
|
270
|
+
# @param shares [Array<String>] the shares to be evaluated
|
271
|
+
# @return [true] returns true if there are enough shares
|
272
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
273
|
+
Contract C::ArrayOf[String] => C::Bool
|
228
274
|
def shares_meet_threshold_min!(shares)
|
229
275
|
fh = Util.extract_share_header(shares.first)
|
230
276
|
unless shares.size >= fh[:threshold]
|
231
277
|
raise TSS::ArgumentError, 'invalid shares, fewer than threshold'
|
278
|
+
else
|
279
|
+
return true
|
232
280
|
end
|
233
281
|
end
|
234
282
|
|
283
|
+
# Were enough shares provided to meet the threshold? They are invalid if not.
|
284
|
+
#
|
285
|
+
# @param shares [Array<String>] the shares to be evaluated
|
286
|
+
# @return [true] returns true if all tests pass
|
287
|
+
Contract C::ArrayOf[String] => C::Bool
|
235
288
|
def validate_all_shares(shares)
|
236
|
-
shares_have_valid_headers!(shares)
|
237
|
-
|
238
|
-
|
239
|
-
|
289
|
+
if shares_have_valid_headers!(shares) &&
|
290
|
+
shares_have_same_bytesize!(shares) &&
|
291
|
+
shares_have_expected_length!(shares) &&
|
292
|
+
shares_meet_threshold_min!(shares)
|
293
|
+
return true
|
294
|
+
else
|
295
|
+
return false
|
296
|
+
end
|
240
297
|
end
|
241
298
|
|
299
|
+
# Do all the shares have a valid first-byte index? They are invalid if not.
|
300
|
+
#
|
301
|
+
# @param shares_bytes [Array<Array>] the shares as Byte Arrays to be evaluated
|
302
|
+
# @return [true] returns true if there are enough shares
|
303
|
+
# @raise [TSS::ArgumentError] if shares appear invalid
|
304
|
+
Contract C::ArrayOf[C::ArrayOf[C::Num]] => C::Bool
|
242
305
|
def shares_bytes_have_valid_indexes!(shares_bytes)
|
243
|
-
u = shares_bytes.
|
306
|
+
u = shares_bytes.map do |s|
|
244
307
|
raise TSS::ArgumentError, 'invalid shares, no index' if s[0].blank?
|
245
308
|
raise TSS::ArgumentError, 'invalid shares, zero index' if s[0] == 0
|
246
309
|
s[0]
|
@@ -248,25 +311,46 @@ module TSS
|
|
248
311
|
|
249
312
|
unless u.uniq.size == shares_bytes.size
|
250
313
|
raise TSS::ArgumentError, 'invalid shares, duplicate indexes'
|
314
|
+
else
|
315
|
+
return true
|
251
316
|
end
|
252
317
|
end
|
253
318
|
|
319
|
+
# Is it valid to use combinations mode? Only when there is an embedded non-zero
|
320
|
+
# hash_id Integer to test the results against. Invalid if not.
|
321
|
+
#
|
322
|
+
# @param hash_id [Integer] the shares as Byte Arrays to be evaluated
|
323
|
+
# @return [true] returns true if OK to use combinations mode
|
324
|
+
# @raise [TSS::ArgumentError] if hash_id represents a non hashing type
|
325
|
+
Contract C::Int => C::Bool
|
254
326
|
def share_combinations_mode_allowed!(hash_id)
|
255
327
|
unless Hasher.codes_without_none.include?(hash_id)
|
256
328
|
raise TSS::ArgumentError, 'invalid options, combinations mode can only be used with hashed shares.'
|
329
|
+
else
|
330
|
+
return true
|
257
331
|
end
|
258
332
|
end
|
259
333
|
|
334
|
+
# Calculate the number of possible combinations when combinations mode is
|
335
|
+
# selected. Raise an exception if the possible combinations are too large.
|
336
|
+
#
|
337
|
+
# If this is not tested, the number of combinations can quickly grow into
|
338
|
+
# numbers that cannot be calculated before the end of the universe.
|
339
|
+
# e.g. 255 total shares, with threshold of 128, results in # combinations of:
|
340
|
+
# 2884329411724603169044874178931143443870105850987581016304218283632259375395
|
341
|
+
#
|
342
|
+
# @param shares [Array<String>] the shares to be evaluated
|
343
|
+
# @param threshold [Integer] the threshold value set in the shares
|
344
|
+
# @param max_combinations [Integer] the max (1_000_000) number of combinations allowed
|
345
|
+
# @return [true] returns true if a reasonable number of combinations
|
346
|
+
# @raise [TSS::ArgumentError] if the number of possible combinations is unreasonably high
|
347
|
+
Contract C::ArrayOf[String], C::Int, C::Int => C::Bool
|
260
348
|
def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000)
|
261
|
-
# Raise if the number of combinations is too high.
|
262
|
-
# If this is not checked, the number of combinations can quickly grow into
|
263
|
-
# numbers that cannot be calculated before the end of the universe.
|
264
|
-
# e.g. 255 total shares, with threshold of 128, results in # combinations of:
|
265
|
-
# 2884329411724603169044874178931143443870105850987581016304218283632259375395
|
266
|
-
#
|
267
349
|
combinations = Util.calc_combinations(shares.size, threshold)
|
268
350
|
if combinations > max_combinations
|
269
351
|
raise TSS::ArgumentError, "invalid options, too many combinations (#{Util.int_commas(combinations)})"
|
352
|
+
else
|
353
|
+
return true
|
270
354
|
end
|
271
355
|
end
|
272
356
|
end
|
data/lib/tss/hasher.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module TSS
|
2
|
+
# Hasher is responsible for managing access to the various one-way hash
|
3
|
+
# functions that can be used to validate a secret.
|
2
4
|
class Hasher
|
3
5
|
HASHES = { NONE: { code: 0, bytesize: 0, hasher: nil },
|
4
6
|
SHA1: { code: 1, bytesize: 20, hasher: Digest::SHA1 },
|
@@ -28,7 +30,7 @@ module TSS
|
|
28
30
|
#
|
29
31
|
# @return [Array<Integer>] all hash codes including NONE
|
30
32
|
def self.codes
|
31
|
-
HASHES.
|
33
|
+
HASHES.map do |_k, v|
|
32
34
|
v[:code]
|
33
35
|
end
|
34
36
|
end
|
@@ -37,7 +39,7 @@ module TSS
|
|
37
39
|
#
|
38
40
|
# @return [Array<Integer>] all hash codes excluding NONE
|
39
41
|
def self.codes_without_none
|
40
|
-
HASHES.
|
42
|
+
HASHES.map do |_k, v|
|
41
43
|
v[:code] if v[:code] > 0
|
42
44
|
end.compact
|
43
45
|
end
|
data/lib/tss/splitter.rb
CHANGED
@@ -1,46 +1,41 @@
|
|
1
1
|
module TSS
|
2
|
-
|
2
|
+
# Splitter has responsibility for splitting a secret into an Array of String shares.
|
3
|
+
class Splitter
|
4
|
+
include Contracts::Core
|
3
5
|
include Util
|
4
6
|
|
5
|
-
|
6
|
-
'a16', :identifier, # String, 16 Bytes, arbitrary binary string (null padded, count is width)
|
7
|
-
'C', :hash_id,
|
8
|
-
'C', :threshold,
|
9
|
-
'n', :share_len
|
10
|
-
])
|
7
|
+
C = Contracts
|
11
8
|
|
12
|
-
|
13
|
-
constructor_type(:schema)
|
9
|
+
attr_reader :secret, :threshold, :num_shares, :identifier, :hash_alg, :format, :pad_blocksize
|
14
10
|
|
15
|
-
|
16
|
-
|
11
|
+
Contract ({ :secret => String, :threshold => C::Maybe[C::Int], :num_shares => C::Maybe[C::Int], :identifier => C::Maybe[String], :hash_alg => C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], :format => C::Maybe[C::Enum['binary', 'human']], :pad_blocksize => C::Maybe[C::Int] }) => C::Any
|
12
|
+
def initialize(opts = {})
|
13
|
+
@secret = opts.fetch(:secret)
|
14
|
+
raise TSS::ArgumentError, 'Invalid secret length. Must be between 1 and 65502' unless @secret.size.between?(1,65502)
|
17
15
|
|
18
|
-
|
19
|
-
.
|
20
|
-
.constrained(lteq: 255)
|
21
|
-
.default(3)
|
16
|
+
@threshold = opts.fetch(:threshold, 3)
|
17
|
+
raise TSS::ArgumentError, 'Invalid threshold size. Must be between 1 and 255' unless @threshold.between?(1,255)
|
22
18
|
|
23
|
-
|
24
|
-
.
|
25
|
-
.constrained(lteq: 255)
|
26
|
-
.default(5)
|
19
|
+
@num_shares = opts.fetch(:num_shares, 5)
|
20
|
+
raise TSS::ArgumentError, 'Invalid num_shares size. Must be between 1 and 255' unless @num_shares.between?(1,255)
|
27
21
|
|
28
|
-
|
29
|
-
|
30
|
-
.
|
31
|
-
.default { SecureRandom.hex(8) }
|
32
|
-
.constrained(format: /^[a-zA-Z0-9\-\_\.]*$/i) # 0 or more of these chars
|
22
|
+
@identifier = opts.fetch(:identifier, SecureRandom.hex(8))
|
23
|
+
raise TSS::ArgumentError, 'Invalid identifier characters' unless @identifier =~ /^[a-zA-Z0-9\-\_\.]*$/i
|
24
|
+
raise TSS::ArgumentError, 'Invalid identifier size. Must be between 0 and 16' unless @identifier.size.between?(0,16)
|
33
25
|
|
34
|
-
|
35
|
-
.
|
26
|
+
@hash_alg = opts.fetch(:hash_alg, 'SHA256')
|
27
|
+
@format = opts.fetch(:format, 'human')
|
36
28
|
|
37
|
-
|
38
|
-
.
|
29
|
+
@pad_blocksize = opts.fetch(:pad_blocksize, 0)
|
30
|
+
raise TSS::ArgumentError, 'Invalid pad_blocksize size. Must be between 0 and 255' unless @pad_blocksize.between?(0,255)
|
31
|
+
end
|
39
32
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
33
|
+
SHARE_HEADER_STRUCT = BinaryStruct.new([
|
34
|
+
'a16', :identifier, # String, 16 Bytes, arbitrary binary string (null padded, count is width)
|
35
|
+
'C', :hash_id,
|
36
|
+
'C', :threshold,
|
37
|
+
'n', :share_len
|
38
|
+
])
|
44
39
|
|
45
40
|
# To split a secret into a set of shares, the following
|
46
41
|
# procedure, or any equivalent method, is used:
|
@@ -112,7 +107,13 @@ module TSS
|
|
112
107
|
#
|
113
108
|
secret_bytes.each do |byte|
|
114
109
|
# Unpack random Byte String into Byte Array of 8 bit unsigned Integers
|
115
|
-
|
110
|
+
# Using a conditional test for now due to bug in sysrandom
|
111
|
+
# https://github.com/cryptosphere/sysrandom/issues/13
|
112
|
+
r = if threshold == 1
|
113
|
+
[]
|
114
|
+
else
|
115
|
+
SecureRandom.random_bytes(threshold - 1).unpack('C*')
|
116
|
+
end
|
116
117
|
|
117
118
|
# Build each share one byte at a time for each byte of the secret.
|
118
119
|
shares.map! { |s| s << Util.f(s[0], [byte] + r) }
|
@@ -132,30 +133,67 @@ module TSS
|
|
132
133
|
|
133
134
|
private
|
134
135
|
|
136
|
+
# The secret must be encoded with UTF-8 of US-ASCII or it is invalid.
|
137
|
+
#
|
138
|
+
# @param secret [String] a secret String
|
139
|
+
# @return [true] returns true if acceptable encoding
|
140
|
+
# @raise [TSS::ArgumentError] if invalid
|
135
141
|
def secret_has_acceptable_encoding!(secret)
|
136
142
|
unless secret.encoding.name == 'UTF-8' || secret.encoding.name == 'US-ASCII'
|
137
143
|
raise TSS::ArgumentError, "invalid secret, must be a UTF-8 or US-ASCII encoded String not '#{secret.encoding.name}'"
|
144
|
+
else
|
145
|
+
return true
|
138
146
|
end
|
139
147
|
end
|
140
148
|
|
149
|
+
# The secret must not being with the padding character or it is invalid.
|
150
|
+
#
|
151
|
+
# @param secret [String] a secret String
|
152
|
+
# @return [true] returns true if String does not begin with padding character
|
153
|
+
# @raise [TSS::ArgumentError] if invalid
|
141
154
|
def secret_does_not_begin_with_padding_char!(secret)
|
142
155
|
if secret.slice(0) == "\u001F"
|
143
156
|
raise TSS::ArgumentError, 'invalid secret, first byte of secret is the reserved left-pad character (\u001F)'
|
157
|
+
else
|
158
|
+
return true
|
144
159
|
end
|
145
160
|
end
|
146
161
|
|
162
|
+
# The num_shares must be greater than or equal to the threshold or it is invalid.
|
163
|
+
#
|
164
|
+
# @param threshold [Integer] the threshold value
|
165
|
+
# @param num_shares [Integer] the num_shares value
|
166
|
+
# @return [true] returns true if num_shares is >= threshold
|
167
|
+
# @raise [TSS::ArgumentError] if invalid
|
147
168
|
def num_shares_not_less_than_threshold!(threshold, num_shares)
|
148
169
|
if num_shares < threshold
|
149
170
|
raise TSS::ArgumentError, "invalid num_shares, must be >= threshold (#{threshold})"
|
171
|
+
else
|
172
|
+
return true
|
150
173
|
end
|
151
174
|
end
|
152
175
|
|
176
|
+
# The total Byte size of the secret, including padding and hash, must be
|
177
|
+
# less than the max allowed Byte size or it is invalid.
|
178
|
+
#
|
179
|
+
# @param secret_bytes [Array<Integer>] the Byte Array containing the secret
|
180
|
+
# @return [true] returns true if num_shares is >= threshold
|
181
|
+
# @raise [TSS::ArgumentError] if invalid
|
153
182
|
def secret_bytes_is_smaller_than_max_size!(secret_bytes)
|
154
183
|
if secret_bytes.size >= 65_535
|
155
184
|
raise TSS::ArgumentError, 'invalid secret, combined padded secret and hash are too large'
|
185
|
+
else
|
186
|
+
return true
|
156
187
|
end
|
157
188
|
end
|
158
189
|
|
190
|
+
# Construct a binary share header from its constituent parts.
|
191
|
+
#
|
192
|
+
# @param identifier [String] the unique identifier String
|
193
|
+
# @param hash_alg [String] the hash algorithm String
|
194
|
+
# @param threshold [Integer] the threshold value
|
195
|
+
# @param share_len [Integer] the length of the share in Bytes
|
196
|
+
# @return [String] returns an octet String of Bytes containing the binary header
|
159
197
|
def share_header(identifier, hash_alg, threshold, share_len)
|
160
198
|
SHARE_HEADER_STRUCT.encode(identifier: identifier,
|
161
199
|
hash_id: Hasher.code(hash_alg),
|
data/lib/tss/tss.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
require 'digest'
|
2
2
|
require 'base64'
|
3
|
-
require 'securerandom'
|
3
|
+
require 'sysrandom/securerandom'
|
4
4
|
require 'binary_struct'
|
5
|
-
require '
|
5
|
+
require 'contracts'
|
6
6
|
require 'tss/blank'
|
7
7
|
require 'tss/version'
|
8
|
-
require 'tss/types'
|
9
8
|
require 'tss/util'
|
10
9
|
require 'tss/hasher'
|
11
10
|
require 'tss/splitter'
|
@@ -81,7 +80,7 @@ module TSS
|
|
81
80
|
|
82
81
|
begin
|
83
82
|
TSS::Splitter.new(opts).split
|
84
|
-
rescue
|
83
|
+
rescue ParamContractError => e
|
85
84
|
raise TSS::ArgumentError, e.message
|
86
85
|
end
|
87
86
|
end
|
@@ -135,7 +134,7 @@ module TSS
|
|
135
134
|
|
136
135
|
begin
|
137
136
|
TSS::Combiner.new(opts).combine
|
138
|
-
rescue
|
137
|
+
rescue ParamContractError => e
|
139
138
|
raise TSS::ArgumentError, e.message
|
140
139
|
end
|
141
140
|
end
|
data/lib/tss/util.rb
CHANGED
@@ -234,24 +234,16 @@ module TSS
|
|
234
234
|
# @return [String] a hex String
|
235
235
|
def self.bytes_to_hex(bytes)
|
236
236
|
hex = ''
|
237
|
-
bytes.each { |b| hex += sprintf('%02x', b)
|
238
|
-
hex
|
237
|
+
bytes.each { |b| hex += sprintf('%02x', b) }
|
238
|
+
hex.downcase
|
239
239
|
end
|
240
240
|
|
241
241
|
# Convert a hex String to an Array of Bytes
|
242
242
|
#
|
243
243
|
# @param str [String] a hex String to convert
|
244
244
|
# @return [Array<Integer>] an Array of Integer Bytes
|
245
|
-
# @raise [TSS::Error] if the hex value is not an even length
|
246
245
|
def self.hex_to_bytes(str)
|
247
|
-
|
248
|
-
strc = str.clone
|
249
|
-
bytes = []
|
250
|
-
len = strc.length
|
251
|
-
raise TSS::Error, 'invalid hex value, cannot be an odd length' if len.odd?
|
252
|
-
# slice off two hex chars at a time and convert them to an Integer Byte.
|
253
|
-
(len / 2).times { bytes << strc.slice!(0, 2).hex }
|
254
|
-
bytes
|
246
|
+
[str].pack('H*').unpack('C*')
|
255
247
|
end
|
256
248
|
|
257
249
|
# Convert a hex String to a UTF-8 String
|
@@ -299,7 +291,7 @@ module TSS
|
|
299
291
|
def self.secure_compare(a, b)
|
300
292
|
return false unless a.bytesize == b.bytesize
|
301
293
|
|
302
|
-
l = a.unpack(
|
294
|
+
l = a.unpack('C*')
|
303
295
|
|
304
296
|
r, i = 0, -1
|
305
297
|
b.each_byte { |v| r |= v ^ l[i+=1] }
|
data/lib/tss/version.rb
CHANGED
data/tss.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.required_ruby_version = '>= 2.1.0'
|
13
13
|
|
14
14
|
cert = File.expand_path('~/.gem-certs/gem-private_key_grempe.pem')
|
15
|
-
if File.exist?(cert)
|
15
|
+
if cert && File.exist?(cert)
|
16
16
|
spec.signing_key = cert
|
17
17
|
spec.cert_chain = ['certs/gem-public_cert_grempe.pem']
|
18
18
|
end
|
@@ -48,13 +48,16 @@ Gem::Specification.new do |spec|
|
|
48
48
|
spec.executables << 'tss'
|
49
49
|
spec.require_paths = ['lib']
|
50
50
|
|
51
|
-
spec.add_dependency '
|
51
|
+
spec.add_dependency 'sysrandom', '~> 1.0'
|
52
|
+
spec.add_dependency 'contracts', '~> 0.14'
|
52
53
|
spec.add_dependency 'binary_struct', '~> 2.1'
|
53
54
|
spec.add_dependency 'thor', '~> 0.19'
|
54
55
|
|
55
|
-
spec.add_development_dependency 'bundler', '~> 1.
|
56
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
56
57
|
spec.add_development_dependency 'rake', '~> 11.1'
|
57
58
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
58
59
|
spec.add_development_dependency 'pry', '~> 0.10'
|
59
|
-
spec.add_development_dependency 'coveralls'
|
60
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
61
|
+
spec.add_development_dependency 'coco', '~> 0.14'
|
62
|
+
spec.add_development_dependency 'wwtd', '~> 1.3'
|
60
63
|
end
|
data.tar.gz.sig
CHANGED
Binary file
|