tss 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +4 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +1156 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +590 -0
- data/Rakefile +23 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/tss +7 -0
- data/certs/gem-public_cert_grempe.pem +21 -0
- data/docs/tss-ietf-draft/draft-mcgrew-tss-03.html +1417 -0
- data/docs/tss-ietf-draft/draft-mcgrew-tss-03.txt +1456 -0
- data/lib/tss.rb +14 -0
- data/lib/tss/blank.rb +142 -0
- data/lib/tss/cli.rb +107 -0
- data/lib/tss/combiner.rb +296 -0
- data/lib/tss/errors.rb +4 -0
- data/lib/tss/hasher.rb +55 -0
- data/lib/tss/splitter.rb +190 -0
- data/lib/tss/tss.rb +15 -0
- data/lib/tss/types.rb +4 -0
- data/lib/tss/util.rb +266 -0
- data/lib/tss/version.rb +3 -0
- data/tss.gemspec +58 -0
- metadata +223 -0
- metadata.gz.sig +0 -0
data/lib/tss.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'base64'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'binary_struct'
|
5
|
+
require 'dry-types'
|
6
|
+
require 'tss/tss'
|
7
|
+
require 'tss/blank'
|
8
|
+
require 'tss/version'
|
9
|
+
require 'tss/types'
|
10
|
+
require 'tss/errors'
|
11
|
+
require 'tss/util'
|
12
|
+
require 'tss/hasher'
|
13
|
+
require 'tss/splitter'
|
14
|
+
require 'tss/combiner'
|
data/lib/tss/blank.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# Extracted from activesupport gem.
|
2
|
+
# https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/activesupport/lib/active_support/core_ext/object/blank.rb
|
3
|
+
class Object
|
4
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
5
|
+
# For example, +false+, '', ' ', +nil+, [], and {} are all blank.
|
6
|
+
#
|
7
|
+
# This simplifies
|
8
|
+
#
|
9
|
+
# !address || address.empty?
|
10
|
+
#
|
11
|
+
# to
|
12
|
+
#
|
13
|
+
# address.blank?
|
14
|
+
#
|
15
|
+
# @return [true, false]
|
16
|
+
def blank?
|
17
|
+
respond_to?(:empty?) ? !!empty? : !self
|
18
|
+
end
|
19
|
+
|
20
|
+
# An object is present if it's not blank.
|
21
|
+
#
|
22
|
+
# @return [true, false]
|
23
|
+
def present?
|
24
|
+
!blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the receiver if it's present otherwise returns +nil+.
|
28
|
+
# <tt>object.presence</tt> is equivalent to
|
29
|
+
#
|
30
|
+
# object.present? ? object : nil
|
31
|
+
#
|
32
|
+
# For example, something like
|
33
|
+
#
|
34
|
+
# state = params[:state] if params[:state].present?
|
35
|
+
# country = params[:country] if params[:country].present?
|
36
|
+
# region = state || country || 'US'
|
37
|
+
#
|
38
|
+
# becomes
|
39
|
+
#
|
40
|
+
# region = params[:state].presence || params[:country].presence || 'US'
|
41
|
+
#
|
42
|
+
# @return [Object]
|
43
|
+
def presence
|
44
|
+
self if present?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class NilClass
|
49
|
+
# +nil+ is blank:
|
50
|
+
#
|
51
|
+
# nil.blank? # => true
|
52
|
+
#
|
53
|
+
# @return [true]
|
54
|
+
def blank?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class FalseClass
|
60
|
+
# +false+ is blank:
|
61
|
+
#
|
62
|
+
# false.blank? # => true
|
63
|
+
#
|
64
|
+
# @return [true]
|
65
|
+
def blank?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class TrueClass
|
71
|
+
# +true+ is not blank:
|
72
|
+
#
|
73
|
+
# true.blank? # => false
|
74
|
+
#
|
75
|
+
# @return [false]
|
76
|
+
def blank?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Array
|
82
|
+
# An array is blank if it's empty:
|
83
|
+
#
|
84
|
+
# [].blank? # => true
|
85
|
+
# [1,2,3].blank? # => false
|
86
|
+
#
|
87
|
+
# @return [true, false]
|
88
|
+
alias_method :blank?, :empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
class Hash
|
92
|
+
# A hash is blank if it's empty:
|
93
|
+
#
|
94
|
+
# {}.blank? # => true
|
95
|
+
# { key: 'value' }.blank? # => false
|
96
|
+
#
|
97
|
+
# @return [true, false]
|
98
|
+
alias_method :blank?, :empty?
|
99
|
+
end
|
100
|
+
|
101
|
+
class String
|
102
|
+
BLANK_RE = /\A[[:space:]]*\z/
|
103
|
+
|
104
|
+
# A string is blank if it's empty or contains whitespaces only:
|
105
|
+
#
|
106
|
+
# ''.blank? # => true
|
107
|
+
# ' '.blank? # => true
|
108
|
+
# "\t\n\r".blank? # => true
|
109
|
+
# ' blah '.blank? # => false
|
110
|
+
#
|
111
|
+
# Unicode whitespace is supported:
|
112
|
+
#
|
113
|
+
# "\u00a0".blank? # => true
|
114
|
+
#
|
115
|
+
# @return [true, false]
|
116
|
+
def blank?
|
117
|
+
BLANK_RE === self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Numeric #:nodoc:
|
122
|
+
# No number is blank:
|
123
|
+
#
|
124
|
+
# 1.blank? # => false
|
125
|
+
# 0.blank? # => false
|
126
|
+
#
|
127
|
+
# @return [false]
|
128
|
+
def blank?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class Time #:nodoc:
|
134
|
+
# No Time is blank:
|
135
|
+
#
|
136
|
+
# Time.now.blank? # => false
|
137
|
+
#
|
138
|
+
# @return [false]
|
139
|
+
def blank?
|
140
|
+
false
|
141
|
+
end
|
142
|
+
end
|
data/lib/tss/cli.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
# Command Line Interface (CLI)
|
4
|
+
# See also, `bin/tss` executable.
|
5
|
+
module TSS
|
6
|
+
class CLI < Thor
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
method_option :threshold, :aliases => '-t', :banner => 'threshold', :type => :numeric, :desc => '# of shares, of total, required to reconstruct a secret'
|
10
|
+
method_option :num_shares, :aliases => '-n', :banner => 'num_shares', :type => :numeric, :desc => '# of shares total that will be generated'
|
11
|
+
method_option :identifier, :aliases => '-i', :banner => 'identifier', :type => :string, :desc => 'A unique identifier string, 0-16 Bytes, [a-zA-Z0-9.-_]'
|
12
|
+
method_option :hash_alg, :aliases => '-h', :banner => 'hash_alg', :type => :string, :desc => 'A hash type for verification, NONE, SHA1, SHA256'
|
13
|
+
method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'human', :desc => 'Share output format, binary or human'
|
14
|
+
method_option :pad_blocksize, :aliases => '-p', :banner => 'pad_blocksize', :type => :numeric, :desc => 'Block size # secrets will be left-padded to, 0-255'
|
15
|
+
desc "split SECRET", "split a SECRET String into shares"
|
16
|
+
long_desc <<-LONGDESC
|
17
|
+
`tss split` will generate a set of Threshold Secret
|
18
|
+
Sharing shares from the SECRET provided. To protect
|
19
|
+
your secret from being saved in your shell history
|
20
|
+
you will be prompted for the single-line secret.
|
21
|
+
|
22
|
+
Optional Params:
|
23
|
+
|
24
|
+
num_shares :
|
25
|
+
The number of total shares that will be generated.
|
26
|
+
|
27
|
+
threshold :
|
28
|
+
The threshold is the number of shares required to
|
29
|
+
recreate a secret. This is always a subset of the total
|
30
|
+
shares.
|
31
|
+
|
32
|
+
identifier :
|
33
|
+
A unique identifier string that will be attached
|
34
|
+
to each share. It can be 0-16 Bytes long and use the
|
35
|
+
characters [a-zA-Z0-9.-_]
|
36
|
+
|
37
|
+
hash_alg :
|
38
|
+
One of NONE, SHA1, SHA256. The algorithm to use for a one-way hash of the secret that will be split along with the secret.
|
39
|
+
|
40
|
+
pad_blocksize :
|
41
|
+
An Integer, 0-255, that represents a multiple to which the secret will be padded. For example if pad_blocksize is set to 8, the secret 'abc' would be left-padded to '00000abc' (the padding char is not zero, that is just for illustration).
|
42
|
+
|
43
|
+
format :
|
44
|
+
Whether to output the shares as a binary octet string (RTSS), or the same encoded as more human friendly Base 64 text with some metadata prefixed.
|
45
|
+
|
46
|
+
Example using all options:
|
47
|
+
|
48
|
+
$ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f human
|
49
|
+
|
50
|
+
Enter your secret:
|
51
|
+
|
52
|
+
secret > my secret
|
53
|
+
|
54
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEBQ-AQG3PuU4oT4qHOh2oJmu-vQwGE6O5hsGRBNtdAYauTIi7VoIdi5imWSrswDdRy
|
55
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADECM0OK5TSamH3nubH3FJ2EGZ4Yux4eQC-mvcYY85oOe6ae3kpvVXjuRUDU1m6sX20X
|
56
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEDb7yF4Vhr1JqNe2Nc8IXo98hmKAxsqC3c_Mn3r3t60NxQMC22ate51StDOM-BImch
|
57
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEEIXU0FajldnRtEQMLK-ZYMO2MRa0NmkBFfNAOx7olbgXLkVbP9txXMDsdokblVwke
|
58
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEFfYo7EcQUOpMH09Ggz_403rvy1r9_ckI_Pd_hm1tRxX8FfzEWyXMAoFCKTOfIKgMo
|
59
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEGDSmh74Ng8WTziMGZXAm5XcpFLqDl2oP4MH24XhYf33IIg1WsPIyMAznI0DJUeLpN
|
60
|
+
LONGDESC
|
61
|
+
def split
|
62
|
+
args = {}
|
63
|
+
|
64
|
+
say('Enter your secret:')
|
65
|
+
args[:secret] = ask('secret > ')
|
66
|
+
args[:threshold] = options[:threshold] if options[:threshold]
|
67
|
+
args[:num_shares] = options[:num_shares] if options[:num_shares]
|
68
|
+
args[:identifier] = options[:identifier] if options[:identifier]
|
69
|
+
args[:hash_alg] = options[:hash_alg] if options[:hash_alg]
|
70
|
+
args[:pad_blocksize] = options[:pad_blocksize] if options[:pad_blocksize]
|
71
|
+
args[:format] = options[:format] if options[:format]
|
72
|
+
|
73
|
+
begin
|
74
|
+
shares = TSS.split(args)
|
75
|
+
shares.each {|s| say(s) }
|
76
|
+
rescue => e
|
77
|
+
say("TSS ERROR : " + e.message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "combine SHARES", "Enter min threshold # of SHARES, one at a time, to reconstruct a split secret"
|
82
|
+
def combine
|
83
|
+
shares = []
|
84
|
+
last_ans = nil
|
85
|
+
|
86
|
+
say('Enter shares, one per line, blank line or dot (.) to finish:')
|
87
|
+
until last_ans == '.' || last_ans == ''
|
88
|
+
last_ans = ask('share> ')
|
89
|
+
shares << last_ans unless last_ans.blank? || last_ans == '.'
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
sec = TSS.combine(shares: shares)
|
94
|
+
|
95
|
+
say('')
|
96
|
+
say('Secret Recovered and Verified!')
|
97
|
+
say('')
|
98
|
+
say("identifier : " + sec[:identifier]) if sec[:identifier].present?
|
99
|
+
say("threshold : " + sec[:threshold].to_s) if sec[:threshold].present?
|
100
|
+
say("processing time (ms) : " + sec[:processing_time_ms].to_s) if sec[:processing_time_ms].present?
|
101
|
+
say("secret :\n" + '*'*50 + "\n" + sec[:secret] + "\n" + '*'*50 + "\n") if sec[:secret].present?
|
102
|
+
rescue => e
|
103
|
+
say("TSS ERROR : " + e.message)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/tss/combiner.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
module TSS
|
2
|
+
class Combiner < Dry::Types::Struct
|
3
|
+
include Util
|
4
|
+
|
5
|
+
# dry-types
|
6
|
+
constructor_type(:schema)
|
7
|
+
|
8
|
+
attribute :shares, Types::Strict::Array
|
9
|
+
.constrained(min_size: 1)
|
10
|
+
.constrained(max_size: 255)
|
11
|
+
.member(Types::Strict::String)
|
12
|
+
|
13
|
+
attribute :select_by, Types::Strict::String
|
14
|
+
.enum('first', 'sample', 'combinations')
|
15
|
+
.default('first')
|
16
|
+
|
17
|
+
# The reconstruction, or combining, operation reconstructs the secret from a
|
18
|
+
# set of valid shares where the number of shares is >= the threshold when the
|
19
|
+
# secret was initially split. All arguments are provided in a single Hash:
|
20
|
+
#
|
21
|
+
# `shares` : The shares parameter is an Array of String shares.
|
22
|
+
#
|
23
|
+
# If the number of shares provided as input to the secret
|
24
|
+
# reconstruction operation is greater than the threshold M, then M
|
25
|
+
# of those shares are selected for use in the operation. The method
|
26
|
+
# used to select the shares can be chosen with the `select_by:` argument
|
27
|
+
# which takes the following values:
|
28
|
+
#
|
29
|
+
# `first` : If X shares are required by the threshold and more than X
|
30
|
+
# shares are provided, then the first X shares in the Array of shares provided
|
31
|
+
# will be used. All others will be discarded and the operation will fail if
|
32
|
+
# those selected shares cannot recreate the secret.
|
33
|
+
#
|
34
|
+
# `sample` : If X shares are required by the threshold and more than X
|
35
|
+
# shares are provided, then X shares will be randomly selected from the Array
|
36
|
+
# of shares provided. All others will be discarded and the operation will
|
37
|
+
# fail if those selected shares cannot recreate the secret.
|
38
|
+
#
|
39
|
+
# `combinations` : If X shares are required, and more than X shares are
|
40
|
+
# provided, then all possible combinations of the threshold number of shares
|
41
|
+
# will be tried to see if the secret can be recreated.
|
42
|
+
# This flexibility comes with a cost. All combinations of `threshold` shares
|
43
|
+
# must be generated. Due to the math associated with combinations it is possible
|
44
|
+
# that the system would try to generate a number of combinations that could never
|
45
|
+
# be generated or processed in many times the life of the Universe. This option
|
46
|
+
# can only be used if the possible combinations for the number of shares and the
|
47
|
+
# threshold needed to reconstruct a secret result in a number of combinations
|
48
|
+
# that is small enough to have a chance at being processed. If the number
|
49
|
+
# of combinations will be too large then the an Exception will be raised before
|
50
|
+
# processing has started.
|
51
|
+
#
|
52
|
+
# If the combine operation does not result in a secret being successfully
|
53
|
+
# extracted, then a `TSS::Error` exception will be raised.
|
54
|
+
#
|
55
|
+
#
|
56
|
+
# How it works:
|
57
|
+
#
|
58
|
+
# To reconstruct a secret from a set of shares, the following
|
59
|
+
# procedure, or any equivalent method, is used:
|
60
|
+
#
|
61
|
+
# If the number of shares provided as input to the secret
|
62
|
+
# reconstruction operation is greater than the threshold M, then M
|
63
|
+
# of those shares are selected for use in the operation. The method
|
64
|
+
# used to select the shares can be arbitrary.
|
65
|
+
#
|
66
|
+
# If the shares are not equal length, then the input is
|
67
|
+
# inconsistent. An error should be reported, and processing must
|
68
|
+
# halt.
|
69
|
+
#
|
70
|
+
# The output string is initialized to the empty (zero-length) octet
|
71
|
+
# string.
|
72
|
+
#
|
73
|
+
# The octet array U is formed by setting U[i] equal to the first
|
74
|
+
# octet of the ith share. (Note that the ordering of the shares is
|
75
|
+
# arbitrary, but must be consistent throughout this algorithm.)
|
76
|
+
#
|
77
|
+
# The initial octet is stripped from each share.
|
78
|
+
#
|
79
|
+
# If any two elements of the array U have the same value, then an
|
80
|
+
# error condition has occurred; this fact should be reported, then
|
81
|
+
# the procedure must halt.
|
82
|
+
#
|
83
|
+
# For each octet of the shares, the following steps are performed.
|
84
|
+
# An array V of M octets is created, in which the array element V[i]
|
85
|
+
# contains the octet from the ith share. The value of I(U, V) is
|
86
|
+
# computed, then appended to the output string.
|
87
|
+
#
|
88
|
+
# The output string is returned.
|
89
|
+
#
|
90
|
+
def combine
|
91
|
+
# unwrap 'human' share format
|
92
|
+
if shares.first.start_with?('tss~')
|
93
|
+
shares.collect! do |s|
|
94
|
+
matcher = /^tss~v1~*[a-zA-Z0-9\.\-\_]{0,16}~[0-9]{1,3}~([a-zA-Z0-9\-\_]+\={0,2})$/
|
95
|
+
s_b64 = s.match(matcher)
|
96
|
+
if s_b64.present?
|
97
|
+
# puts s_b64.to_a[1].inspect
|
98
|
+
Base64.urlsafe_decode64(s_b64.to_a[1])
|
99
|
+
else
|
100
|
+
raise TSS::ArgumentError, 'invalid shares, human format shares do not match expected pattern'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
validate_all_shares(shares)
|
106
|
+
orig_shares_size = shares.size
|
107
|
+
start_processing_time = Time.now
|
108
|
+
|
109
|
+
h = Util.extract_share_header(shares.sample)
|
110
|
+
threshold = h[:threshold]
|
111
|
+
identifier = h[:identifier]
|
112
|
+
hash_id = h[:hash_id]
|
113
|
+
|
114
|
+
# If there are more shares than the threshold would require
|
115
|
+
# then choose a subset of the shares based on preference.
|
116
|
+
if shares.size > threshold
|
117
|
+
case select_by
|
118
|
+
when 'first'
|
119
|
+
@shares = shares.shift(threshold)
|
120
|
+
when 'sample'
|
121
|
+
@shares = shares.sample(threshold)
|
122
|
+
when 'combinations'
|
123
|
+
share_combinations_mode_allowed?(hash_id)
|
124
|
+
share_combinations_out_of_bounds?(shares, threshold)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# slice out the data after the header bytes in each share
|
129
|
+
# and unpack the byte string into an Array of Byte Arrays
|
130
|
+
shares_bytes = shares.collect do |s|
|
131
|
+
bytestring = s.byteslice(Splitter::SHARE_HEADER_STRUCT.size..s.bytesize)
|
132
|
+
bytestring.unpack('C*') unless bytestring.nil?
|
133
|
+
end.compact
|
134
|
+
|
135
|
+
shares_bytes_have_valid_indexes?(shares_bytes)
|
136
|
+
|
137
|
+
if select_by == 'combinations'
|
138
|
+
# Build an Array of all possible `threshold` size combinations.
|
139
|
+
share_combos = shares_bytes.combination(threshold).to_a
|
140
|
+
|
141
|
+
secret = nil
|
142
|
+
while secret.nil? && share_combos.present?
|
143
|
+
# Check a combination and shift it off the Array
|
144
|
+
result = extract_secret_from_shares(hash_id, share_combos.shift)
|
145
|
+
next if result.nil?
|
146
|
+
secret = result
|
147
|
+
end
|
148
|
+
else
|
149
|
+
secret = extract_secret_from_shares(hash_id, shares_bytes)
|
150
|
+
end
|
151
|
+
|
152
|
+
if secret.present?
|
153
|
+
{
|
154
|
+
hash_alg: Hasher.key_from_code(hash_id).to_s,
|
155
|
+
identifier: identifier,
|
156
|
+
num_shares_provided: orig_shares_size,
|
157
|
+
num_shares_used: share_combos.present? ? share_combos.first.size : shares.size,
|
158
|
+
processing_started_at: start_processing_time.utc.iso8601,
|
159
|
+
processing_finished_at: Time.now.utc.iso8601,
|
160
|
+
processing_time_ms: ((Time.now - start_processing_time)*1000).round(2),
|
161
|
+
secret: Util.bytes_to_utf8(secret),
|
162
|
+
shares_select_by: select_by,
|
163
|
+
combinations: share_combos.present? ? share_combos.size : nil,
|
164
|
+
threshold: threshold
|
165
|
+
}
|
166
|
+
else
|
167
|
+
raise TSS::Error, 'unable to recombine shares into a verifiable secret'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def extract_secret_from_shares(hash_id, shares_bytes)
|
174
|
+
secret = []
|
175
|
+
|
176
|
+
# build up an Array of index values from each share
|
177
|
+
# u[i] equal to the first octet of the ith share
|
178
|
+
u = shares_bytes.collect { |s| s[0] }
|
179
|
+
|
180
|
+
# loop through each byte in all the shares
|
181
|
+
# start at Array index 1 in each share's Byte Array to skip the index
|
182
|
+
(1..(shares_bytes.first.length - 1)).each do |i|
|
183
|
+
v = shares_bytes.collect { |share| share[i] }
|
184
|
+
secret << Util.lagrange_interpolation(u, v)
|
185
|
+
end
|
186
|
+
|
187
|
+
strip_left_pad(secret)
|
188
|
+
|
189
|
+
hash_alg = Hasher.key_from_code(hash_id)
|
190
|
+
|
191
|
+
# Run the hash digest checks if the shares were created with a digest
|
192
|
+
if Hasher.codes_without_none.include?(hash_id)
|
193
|
+
# RTSS : pop off the hash digest bytes from the tail of the secret. This
|
194
|
+
# leaves `secret` with only the secret bytes remaining.
|
195
|
+
orig_hash_bytes = secret.pop(Hasher.bytesize(hash_alg))
|
196
|
+
|
197
|
+
# RTSS : verify that the recombined secret computes the same hash
|
198
|
+
# digest now as when it was originally created.
|
199
|
+
new_hash_bytes = Hasher.byte_array(hash_alg, Util.bytes_to_utf8(secret))
|
200
|
+
|
201
|
+
# return the secret only if the hash test passed
|
202
|
+
new_hash_bytes == orig_hash_bytes ? secret : nil
|
203
|
+
else
|
204
|
+
secret
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# strip off leading padding chars ("\u001F", decimal 31)
|
209
|
+
def strip_left_pad(secret)
|
210
|
+
secret.shift while secret.first == 31
|
211
|
+
end
|
212
|
+
|
213
|
+
def valid_header?(header)
|
214
|
+
header.is_a?(Hash) &&
|
215
|
+
header.key?(:identifier) &&
|
216
|
+
header[:identifier].is_a?(String) &&
|
217
|
+
header.key?(:hash_id) &&
|
218
|
+
header[:hash_id].is_a?(Integer) &&
|
219
|
+
header.key?(:threshold) &&
|
220
|
+
header[:threshold].is_a?(Integer) &&
|
221
|
+
header.key?(:share_len) &&
|
222
|
+
header[:share_len].is_a?(Integer)
|
223
|
+
end
|
224
|
+
|
225
|
+
def shares_have_same_bytesize?(shares)
|
226
|
+
shares.each do |s|
|
227
|
+
unless s.bytesize == shares.first.bytesize
|
228
|
+
raise TSS::ArgumentError, 'invalid shares, different byte lengths'
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def shares_have_valid_headers?(shares)
|
234
|
+
fh = Util.extract_share_header(shares.first)
|
235
|
+
shares.each do |s|
|
236
|
+
h = Util.extract_share_header(s)
|
237
|
+
unless valid_header?(h) && h == fh
|
238
|
+
raise TSS::ArgumentError, 'invalid shares, bad headers'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def shares_have_expected_length?(shares)
|
244
|
+
shares.each do |s|
|
245
|
+
unless s.bytesize > Splitter::SHARE_HEADER_STRUCT.size + 1
|
246
|
+
raise TSS::ArgumentError, 'invalid shares, too short'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def shares_meet_threshold_min?(shares)
|
252
|
+
fh = Util.extract_share_header(shares.first)
|
253
|
+
unless shares.size >= fh[:threshold]
|
254
|
+
raise TSS::ArgumentError, 'invalid shares, fewer than threshold'
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def validate_all_shares(shares)
|
259
|
+
shares_have_valid_headers?(shares)
|
260
|
+
shares_have_same_bytesize?(shares)
|
261
|
+
shares_have_expected_length?(shares)
|
262
|
+
shares_meet_threshold_min?(shares)
|
263
|
+
end
|
264
|
+
|
265
|
+
def shares_bytes_have_valid_indexes?(shares_bytes)
|
266
|
+
u = shares_bytes.collect do |s|
|
267
|
+
raise TSS::ArgumentError, 'invalid shares, no index' if s[0].blank?
|
268
|
+
raise TSS::ArgumentError, 'invalid shares, zero index' if s[0] == 0
|
269
|
+
s[0]
|
270
|
+
end
|
271
|
+
|
272
|
+
unless u.uniq.size == shares_bytes.size
|
273
|
+
raise TSS::ArgumentError, 'invalid shares, duplicate indexes'
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def share_combinations_mode_allowed?(hash_id)
|
278
|
+
unless Hasher.codes_without_none.include?(hash_id)
|
279
|
+
raise TSS::ArgumentError, 'invalid options, combinations mode can only be used with hashed shares.'
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def share_combinations_out_of_bounds?(shares, threshold, max_combinations = 1_000_000)
|
284
|
+
# Raise if the number of combinations is too high.
|
285
|
+
# If this is not checked, the number of combinations can quickly grow into
|
286
|
+
# numbers that cannot be calculated before the end of the universe.
|
287
|
+
# e.g. 255 total shares, with threshold of 128, results in # combinations of:
|
288
|
+
# 2884329411724603169044874178931143443870105850987581016304218283632259375395
|
289
|
+
#
|
290
|
+
combinations = Util.calc_combinations(shares.size, threshold)
|
291
|
+
if combinations > max_combinations
|
292
|
+
raise TSS::ArgumentError, "invalid options, too many combinations (#{Util.int_commas(combinations)})"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|