tss 0.1.1 → 0.2.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 +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
|