tss 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tss/combiner.rb CHANGED
@@ -1,18 +1,24 @@
1
1
  module TSS
2
- class Combiner < Dry::Types::Struct
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
- # dry-types
6
- constructor_type(:schema)
10
+ C = Contracts
7
11
 
8
- attribute :shares, Types::Strict::Array
9
- .constrained(min_size: 1)
10
- .constrained(max_size: 255)
11
- .member(Types::Strict::String)
12
+ attr_reader :shares, :select_by
12
13
 
13
- attribute :select_by, Types::Strict::String
14
- .enum('first', 'sample', 'combinations')
15
- .default('first')
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
- # If there are more shares than the threshold would require
65
- # then choose a subset of the shares based on preference.
66
- if shares.size > threshold
67
- case select_by
68
- when 'first'
69
- @shares = shares.shift(threshold)
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.collect do |s|
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
- hash_alg: Hasher.key_from_code(hash_id).to_s,
108
+ hash: secret[:hash],
109
+ hash_alg: secret[:hash_alg].to_s,
104
110
  identifier: identifier,
105
- num_shares_provided: orig_shares_size,
106
- num_shares_used: share_combos.present? ? share_combos.first.size : shares.size,
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.collect { |s| s[0] }
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.collect { |share| share[i] }
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
- if Util.secure_compare(Util.bytes_to_hex(orig_hash_bytes), Util.bytes_to_hex(new_hash_bytes))
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
- if secret.present?
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
- # strip off leading padding chars ("\u001F", decimal 31)
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.collect do |s|
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
- def valid_header?(header)
191
- header.is_a?(Hash) &&
192
- header.key?(:identifier) &&
193
- header[:identifier].is_a?(String) &&
194
- header.key?(:hash_id) &&
195
- header[:hash_id].is_a?(Integer) &&
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
- h = Util.extract_share_header(s)
214
- unless valid_header?(h) && h == fh
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
- shares_have_same_bytesize!(shares)
238
- shares_have_expected_length!(shares)
239
- shares_meet_threshold_min!(shares)
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.collect do |s|
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.collect do |_k, v|
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.collect do |_k, v|
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
- class Splitter < Dry::Types::Struct
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
- SHARE_HEADER_STRUCT = BinaryStruct.new([
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
- # dry-types
13
- constructor_type(:schema)
9
+ attr_reader :secret, :threshold, :num_shares, :identifier, :hash_alg, :format, :pad_blocksize
14
10
 
15
- attribute :secret, Types::Strict::String
16
- .constrained(min_size: 1)
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
- attribute :threshold, Types::Coercible::Int
19
- .constrained(gteq: 1)
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
- attribute :num_shares, Types::Coercible::Int
24
- .constrained(gteq: 1)
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
- attribute :identifier, Types::Strict::String
29
- .constrained(min_size: 0)
30
- .constrained(max_size: 16)
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
- attribute :hash_alg, Types::Strict::String.enum('NONE', 'SHA1', 'SHA256')
35
- .default('SHA256')
26
+ @hash_alg = opts.fetch(:hash_alg, 'SHA256')
27
+ @format = opts.fetch(:format, 'human')
36
28
 
37
- attribute :format, Types::Strict::String.enum('binary', 'human')
38
- .default('binary')
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
- attribute :pad_blocksize, Types::Coercible::Int
41
- .constrained(gteq: 0)
42
- .constrained(lteq: 255)
43
- .default(0)
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
- r = SecureRandom.random_bytes(threshold - 1).unpack('C*')
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 'dry-types'
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 Dry::Types::ConstraintError => e
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 Dry::Types::ConstraintError => e
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).upcase }
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
- # clone so we don't destroy the original string passed in by slicing it.
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("C*")
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
@@ -1,3 +1,3 @@
1
1
  module TSS
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
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 'dry-types', '~> 0.7'
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.11'
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