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