tss 0.1.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 +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
|