tss 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd575aedfcbdae566a0c2005c129da4e670b0c50
4
- data.tar.gz: 85801386f03fcd06b29acea57c166149ddf22bb8
3
+ metadata.gz: 53a46eaf5b6ac5b816d288d0ed9379dc59679145
4
+ data.tar.gz: 19fa36c7c3ef3d6e42c5a69cbe8a227a4985dfb8
5
5
  SHA512:
6
- metadata.gz: 6727e7bd3a7d758cccc93b780adb028164ca51cf47f56fb41f4e20468cbefb7d73de054f90c199e82ff49ac3091c438ea900efd05977d39bb4cb83cab510ff86
7
- data.tar.gz: 9f6dbd3a9061eb6fe986042e39591dc7a0a2ecf9bdb139ab48220631cd1d966603ccdf3c75d3d2f09528580bdfab665e072e3431ec5c479b0820b5f3685115b2
6
+ metadata.gz: 88160d1aedccbac904ee0850fe987e85c67de3754ba6182d646932d3c8ec3de22098bfc99309371706a5b7e291c878dfff6cb00afec6c584f162abb8cf4d6689
7
+ data.tar.gz: 66987ba2804b48a4a8d5ba79065e719c78aaf8c0deaec74974b5fa4a5a48127ff72a5d6d2b7cf84920995dac7c7d842b808b0763ce179bf60714b75989ea2668
checksums.yaml.gz.sig CHANGED
Binary file
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # TSS - Threshold Secret Sharing
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/tss.svg)](https://badge.fury.io/rb/tss)
3
4
  [![Build Status](https://travis-ci.org/grempe/tss-rb.svg?branch=master)](https://travis-ci.org/grempe/tss-rb)
4
5
  [![Coverage Status](https://coveralls.io/repos/github/grempe/tss-rb/badge.svg?branch=master)](https://coveralls.io/github/grempe/tss-rb?branch=master)
5
6
  [![Code Climate](https://codeclimate.com/github/grempe/tss-rb/badges/gpa.svg)](https://codeclimate.com/github/grempe/tss-rb)
6
7
 
7
- ## WARNING : PRE-ALPHA CODE
8
+ ## WARNING : BETA CODE
8
9
 
9
- This code is currently a work in progress and is not yet ready for production
10
- use. The API, input and output formats, and other aspects are likely to change
11
- before release. There has been no security review of this code.
10
+ This code is new and has not yet been tested in production. Use at your own risk.
11
+ The share format and interface should be fairly stable now but should not be
12
+ considered fully stable until v1.0.0 is released.
12
13
 
13
14
  ## About TSS
14
15
 
@@ -43,6 +44,112 @@ hash for the secret is not available to shareholders prior to recombining shares
43
44
  The specification also addresses the optional implementation of a `MAGIC_NUMBER` and
44
45
  advanced error correction schemes. These extras are not currently implemented.
45
46
 
47
+ ## TL;DR
48
+
49
+ No time for docs? Here is how to get going in 10 seconds or less with the
50
+ CLI or in Ruby. The CLI defaults to using `human` shares, and Ruby defaults
51
+ to a binary octet string representation. The default is `3 out of 5` threshold
52
+ sharing.
53
+
54
+ ### CLI (Human Shares)
55
+
56
+ ```text
57
+ ~/src$ gem install tss
58
+ Successfully installed tss-0.1.0
59
+ 1 gem installed
60
+ ~/src$ tss split
61
+ Enter your secret:
62
+ secret > my deep dark secret
63
+ tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQBDoW7GJ66g6nQHQZVM_iUxMVEO7NHlwDaEM5FYsVwhBSfio-WF-w2gqSKRjBp6YyqTQKR
64
+ tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQCxKBLxPsXuW4e7xE0zKiso49aEyuMKNIhjISe7ga865KDnBBpE1iZ6ESUkaWojKE3yNbc
65
+ tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQDp1zQuADISueqk2UK3yNdBDh7XGlyoD2R6X9y-BCoI7iwAE02A8aj8vKO9ticeJpQMvDi
66
+ tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQEgzj1RJXwKbu0pa5Z5qssmoX0cz22gVg8UCc6tasiqbDNi7bq_xKUczpYuc7utwDyPxV1
67
+ tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQF4MRuOG4v2jIA2dpn9SDdPTLVPH9ICbeMNdzWo702YZr-F-u174yuaYxC3rPaQzuVxTNL
68
+ ~/src$ tss combine
69
+ Enter shares, one per line, blank line or dot (.) to finish:
70
+ share> tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQBDoW7GJ66g6nQHQZVM_iUxMVEO7NHlwDaEM5FYsVwhBSfio-WF-w2gqSKRjBp6YyqTQKR
71
+ share> tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQCxKBLxPsXuW4e7xE0zKiso49aEyuMKNIhjISe7ga865KDnBBpE1iZ6ESUkaWojKE3yNbc
72
+ share> tss~v1~4a993275528d5ec7~3~NGE5OTMyNzU1MjhkNWVjNwIDADQDp1zQuADISueqk2UK3yNdBDh7XGlyoD2R6X9y-BCoI7iwAE02A8aj8vKO9ticeJpQMvDi
73
+ share> .
74
+
75
+ Secret Recovered and Verified!
76
+
77
+ identifier : 4a993275528d5ec7
78
+ threshold : 3
79
+ processing time (ms) : 0.64
80
+ secret :
81
+ **************************************************
82
+ my deep dark secret
83
+ **************************************************
84
+ ```
85
+
86
+ ### Ruby (Binary Octet Shares)
87
+
88
+ ```text
89
+ ~/src$ irb
90
+ irb(main):001:0> require 'tss'
91
+ => true
92
+ irb(main):002:0> shares = TSS.split(secret: 'my deep dark secret')
93
+ => ["ab87eb60ae14dd87\x02\x03\x004\x01\xC6+\xC8\x9F\xE4\x7F\x85\x17\xBD\xF6\xE6\xE3m\xB9\xFF\x8CGoS\x90\xB0{\xAB\x04N\xE2\x8F\xA0\xDC\x06\xC7Y\xBE\xCD?\xBDe9\xF3\xDF\xEA\xC9s\x105\xA4\xD8TZw\x9E", "ab87eb60ae14dd87\x02\x03\x004\x02T\xBB\xEF\x12\x81\xE2\xD2\x8Et\x95\x8Eg\xE6x=HD8\xAD\xE5\xF2'OdBO4vL\xF90\xA5c\x82\xE8\x11\x94\x8E\xEEV\xB3\xAFh\xB7\x80Ac\x15\xD9\xC7\x93", "ab87eb60ae14dd87\x02\x03\x004\x03\xFF\xE9\a\xE9\x00\xF8'\xB9\xAD\x02\x1A\xEF\xAB\xB2\xA7\xA7q2\x8A\x84\xFBC\v\ny\x98\x12\xA2C\x9B\xBB\xC2qY\x05e\xF6\xC5\x11\x11K\xF6:\xEA\xE8\xF8\f\x8C4\x94\xA2", "ab87eb60ae14dd87\x02\x03\x004\x04\xD4e\xB5\xD2o\x8AxJ\x96\xBB\x80o\xDCC\x12\xA0u\xE0\xB7\xACP\x82\x14\x13\x04\xD0\xE1\x82\xC4:k\\\xA8\xC1g\xA2}\"\xCF\x04x\xEC*\xB9\xC8q,\x8F\xE1\xF6\xB4", "ab87eb60ae14dd87\x02\x03\x004\x05\x7F7])\xEE\x90\x8D}O,\x14\xE7\x91\x89\x88O@\xEA\x90\xCDY\xE6P}?\a\xC7V\xCBX\xE0;\xBA\x1A\x8A\xD6\x1Fi0C\x80\xB5x\xE4\xA0\xC8C\x16\f\xA5\x85"]
94
+ irb(main):003:0> secret = TSS.combine(shares: shares)
95
+ => {:hash_alg=>"SHA256", :identifier=>"ab87eb60ae14dd87", :num_shares_provided=>5, :num_shares_used=>3, :processing_started_at=>"2016-04-13T19:37:14Z", :processing_finished_at=>"2016-04-13T19:37:14Z", :processing_time_ms=>0.63, :secret=>"my deep dark secret", :shares_select_by=>"first", :combinations=>nil, :threshold=>3}
96
+ irb(main):004:0> puts secret[:secret]
97
+ my deep dark secret
98
+ => nil
99
+ ```
100
+
101
+ ## Is it any good?
102
+
103
+ While this implementation has not had a formal security review, the cryptographic
104
+ underpinnings were carefully specified in an IETF draft document authored by a
105
+ noted cryptographer. I have reached out to individuals respected in the field
106
+ for their work in implementing cryptographic solutions to help review this code.
107
+
108
+ > I've read draft-mcgrew-tss-03 and then took a look at your code.
109
+ > Impressive! Nice docs, clean easy-to-read code. I'd use constant-time
110
+ > comparison for hashes [[resolved : 254ecab](https://github.com/grempe/tss-rb/commit/254ecab24a338872a5b05c7446213ef1ddabf4cb)],
111
+ > but apart from that I have nothing to add. Good job!
112
+ >
113
+ > -- Dmitry Chestnykh ([@dchest](https://github.com/dchest))
114
+ >
115
+ > [v0.1.0 : 4/13/2016]
116
+
117
+ All that being said, if your threat model includes a **N**ation **S**tate **A**ctor
118
+ the security of this particular code should probably not be your primary concern.
119
+
120
+ ## Suggestions for Use
121
+
122
+ * Don't split large texts. Instead, split the much smaller encryption
123
+ keys that protect encrypted large texts. Supply the encrypted
124
+ files and the shares separately to recipients. Threshold secret sharing can be
125
+ very slow at splitting and recombining very large bodies of text, especially
126
+ when combined with a large number of shares. Every byte of the secret must
127
+ be processed `num_shares` times.
128
+
129
+ * Don't treat shares like encrypted data, but instead like the encryption keys
130
+ that unlock the data. Shares are keys, and need to be protected as such. There is
131
+ nothing to slow down an attacker if they have access to enough shares.
132
+
133
+ * If you send keys by email, or some other insecure channel, then your email
134
+ provider, or any entity with access to their data, now also has the keys to
135
+ your data. They just need to collect enough keys to meet the threshold.
136
+
137
+ * Use public key cryptography to encrypt secret shares with the public key of
138
+ each individual recipient. This can protect the share data from unwanted use while
139
+ in transit or at rest. Excellent choices might be
140
+ [RbNaCl](https://github.com/cryptosphere/rbnacl)
141
+ or [TweetNaCl.js](https://github.com/dchest/tweetnacl-js).
142
+
143
+ * Put careful thought into how you want to distribute shares. It often makes
144
+ sense to give individuals more than one share.
145
+
146
+ ## Supported Platforms
147
+
148
+ TSS is continuously integration tested on the following Ruby VMs:
149
+
150
+ * MRI 2.1, 2.2, 2.3
151
+
152
+ It may work on others as well.
46
153
 
47
154
  ## Installation
48
155
 
@@ -112,30 +219,6 @@ You can also clone the repository and verify the signatures locally using your
112
219
  own GnuPG installation. You can find my certificates and read about how to conduct
113
220
  this verification at [https://www.rempe.us/keys/](https://www.rempe.us/keys/).
114
221
 
115
- ## TSS : Suggestions for Use
116
-
117
- * Don't split large texts. Instead, split the much smaller encryption
118
- keys that protect encrypted large texts. Supply the encrypted
119
- files and the shares separately to recipients. Threshold secret sharing can be
120
- very slow at splitting and recombining very large bodies of text, especially
121
- when combined with a large number of shares. Every byte of the secret must
122
- be processed `num_shares` times.
123
-
124
- * Don't treat shares like encrypted data, but instead like the encryption keys
125
- that unlock the data. Shares are keys, and need to be protected as such. There is
126
- nothing to slow down an attacker if they have access to enough shares.
127
-
128
- * If you send keys by email, or some other insecure channel, then your email
129
- provider, or any entity with access to their data, now also has the keys to
130
- your data. They just need to collect enough keys to meet the threshold.
131
-
132
- * Use public key cryptography to encrypt secret shares with the public key of
133
- each individual recipient. This can protect the share data from unwanted use while
134
- in transit or at rest.
135
-
136
- * Put careful thought into how you want to distribute shares. It often makes
137
- sense to give individuals more than one share.
138
-
139
222
  ## Command Line Interface
140
223
 
141
224
  When you install the gem a simple `tss` command-line interface (CLI)
@@ -547,6 +630,9 @@ run `bundle exec rake release`, which will create a git tag for the version,
547
630
  push git commits and tags, and push the `.gem` file
548
631
  to [rubygems.org](https://rubygems.org).
549
632
 
633
+ You can run the Command Line Interface (CLI) in development
634
+ with `bundle exec bin/tss`.
635
+
550
636
  ### Contributing
551
637
 
552
638
  Bug reports and pull requests are welcome on GitHub
data/lib/tss/combiner.rb CHANGED
@@ -14,92 +14,42 @@ module TSS
14
14
  .enum('first', 'sample', 'combinations')
15
15
  .default('first')
16
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:
17
+ # To reconstruct a secret from a set of shares, the following
18
+ # procedure, or any equivalent method, is used:
20
19
  #
21
- # `shares` : The shares parameter is an Array of String shares.
20
+ # If the number of shares provided as input to the secret
21
+ # reconstruction operation is greater than the threshold M, then M
22
+ # of those shares are selected for use in the operation. The method
23
+ # used to select the shares can be arbitrary.
22
24
  #
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:
25
+ # If the shares are not equal length, then the input is
26
+ # inconsistent. An error should be reported, and processing must
27
+ # halt.
28
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.
29
+ # The output string is initialized to the empty (zero-length) octet
30
+ # string.
33
31
  #
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.
32
+ # The octet array U is formed by setting U[i] equal to the first
33
+ # octet of the ith share. (Note that the ordering of the shares is
34
+ # arbitrary, but must be consistent throughout this algorithm.)
38
35
  #
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.
36
+ # The initial octet is stripped from each share.
51
37
  #
52
- # If the combine operation does not result in a secret being successfully
53
- # extracted, then a `TSS::Error` exception will be raised.
38
+ # If any two elements of the array U have the same value, then an
39
+ # error condition has occurred; this fact should be reported, then
40
+ # the procedure must halt.
54
41
  #
42
+ # For each octet of the shares, the following steps are performed.
43
+ # An array V of M octets is created, in which the array element V[i]
44
+ # contains the octet from the ith share. The value of I(U, V) is
45
+ # computed, then appended to the output string.
55
46
  #
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.
47
+ # The output string is returned.
89
48
  #
90
49
  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
50
+ # unwrap 'human' shares into binary shares
51
+ if all_shares_appear_human?(shares)
52
+ @shares = convert_shares_human_to_binary(shares)
103
53
  end
104
54
 
105
55
  validate_all_shares(shares)
@@ -120,8 +70,8 @@ module TSS
120
70
  when 'sample'
121
71
  @shares = shares.sample(threshold)
122
72
  when 'combinations'
123
- share_combinations_mode_allowed?(hash_id)
124
- share_combinations_out_of_bounds?(shares, threshold)
73
+ share_combinations_mode_allowed!(hash_id)
74
+ share_combinations_out_of_bounds!(shares, threshold)
125
75
  end
126
76
  end
127
77
 
@@ -132,7 +82,7 @@ module TSS
132
82
  bytestring.unpack('C*') unless bytestring.nil?
133
83
  end.compact
134
84
 
135
- shares_bytes_have_valid_indexes?(shares_bytes)
85
+ shares_bytes_have_valid_indexes!(shares_bytes)
136
86
 
137
87
  if select_by == 'combinations'
138
88
  # Build an Array of all possible `threshold` size combinations.
@@ -141,36 +91,32 @@ module TSS
141
91
  secret = nil
142
92
  while secret.nil? && share_combos.present?
143
93
  # Check a combination and shift it off the Array
144
- result = extract_secret_from_shares(hash_id, share_combos.shift)
94
+ result = extract_secret_from_shares!(hash_id, share_combos.shift)
145
95
  next if result.nil?
146
96
  secret = result
147
97
  end
148
98
  else
149
- secret = extract_secret_from_shares(hash_id, shares_bytes)
99
+ secret = extract_secret_from_shares!(hash_id, shares_bytes)
150
100
  end
151
101
 
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
102
+ {
103
+ hash_alg: Hasher.key_from_code(hash_id).to_s,
104
+ 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,
113
+ threshold: threshold
114
+ }
169
115
  end
170
116
 
171
117
  private
172
118
 
173
- def extract_secret_from_shares(hash_id, shares_bytes)
119
+ def extract_secret_from_shares!(hash_id, shares_bytes)
174
120
  secret = []
175
121
 
176
122
  # build up an Array of index values from each share
@@ -198,10 +144,17 @@ module TSS
198
144
  # digest now as when it was originally created.
199
145
  new_hash_bytes = Hasher.byte_array(hash_alg, Util.bytes_to_utf8(secret))
200
146
 
201
- # return the secret only if the hash test passed
202
- new_hash_bytes == orig_hash_bytes ? secret : nil
147
+ if Util.secure_compare(Util.bytes_to_hex(orig_hash_bytes), Util.bytes_to_hex(new_hash_bytes))
148
+ return secret
149
+ else
150
+ raise TSS::InvalidSecretHashError, 'invalid shares, hash of secret does not equal embedded hash'
151
+ end
203
152
  else
204
- secret
153
+ if secret.present?
154
+ return secret
155
+ else
156
+ raise TSS::NoSecretError, 'invalid shares, unable to recombine into a verifiable secret'
157
+ end
205
158
  end
206
159
  end
207
160
 
@@ -210,6 +163,30 @@ module TSS
210
163
  secret.shift while secret.first == 31
211
164
  end
212
165
 
166
+ def all_shares_appear_human?(shares)
167
+ shares.all? do |s|
168
+ # test for starting with 'tss' since regex match against
169
+ # binary data sometimes throws exceptions.
170
+ s.start_with?('tss~') && s.match(Util::HUMAN_SHARE_RE)
171
+ end
172
+ end
173
+
174
+ def convert_shares_human_to_binary(shares)
175
+ shares.collect do |s|
176
+ s_b64 = s.match(Util::HUMAN_SHARE_RE)
177
+ if s_b64.present? && s_b64.to_a[1].present?
178
+ begin
179
+ # the [1] capture group contains the Base64 encoded bin share
180
+ Base64.urlsafe_decode64(s_b64.to_a[1])
181
+ rescue ArgumentError
182
+ raise TSS::ArgumentError, 'invalid shares, some human format shares have invalid Base64 data'
183
+ end
184
+ else
185
+ raise TSS::ArgumentError, 'invalid shares, some human format shares do not match expected pattern'
186
+ end
187
+ end
188
+ end
189
+
213
190
  def valid_header?(header)
214
191
  header.is_a?(Hash) &&
215
192
  header.key?(:identifier) &&
@@ -222,7 +199,7 @@ module TSS
222
199
  header[:share_len].is_a?(Integer)
223
200
  end
224
201
 
225
- def shares_have_same_bytesize?(shares)
202
+ def shares_have_same_bytesize!(shares)
226
203
  shares.each do |s|
227
204
  unless s.bytesize == shares.first.bytesize
228
205
  raise TSS::ArgumentError, 'invalid shares, different byte lengths'
@@ -230,7 +207,7 @@ module TSS
230
207
  end
231
208
  end
232
209
 
233
- def shares_have_valid_headers?(shares)
210
+ def shares_have_valid_headers!(shares)
234
211
  fh = Util.extract_share_header(shares.first)
235
212
  shares.each do |s|
236
213
  h = Util.extract_share_header(s)
@@ -240,7 +217,7 @@ module TSS
240
217
  end
241
218
  end
242
219
 
243
- def shares_have_expected_length?(shares)
220
+ def shares_have_expected_length!(shares)
244
221
  shares.each do |s|
245
222
  unless s.bytesize > Splitter::SHARE_HEADER_STRUCT.size + 1
246
223
  raise TSS::ArgumentError, 'invalid shares, too short'
@@ -248,7 +225,7 @@ module TSS
248
225
  end
249
226
  end
250
227
 
251
- def shares_meet_threshold_min?(shares)
228
+ def shares_meet_threshold_min!(shares)
252
229
  fh = Util.extract_share_header(shares.first)
253
230
  unless shares.size >= fh[:threshold]
254
231
  raise TSS::ArgumentError, 'invalid shares, fewer than threshold'
@@ -256,13 +233,13 @@ module TSS
256
233
  end
257
234
 
258
235
  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)
236
+ shares_have_valid_headers!(shares)
237
+ shares_have_same_bytesize!(shares)
238
+ shares_have_expected_length!(shares)
239
+ shares_meet_threshold_min!(shares)
263
240
  end
264
241
 
265
- def shares_bytes_have_valid_indexes?(shares_bytes)
242
+ def shares_bytes_have_valid_indexes!(shares_bytes)
266
243
  u = shares_bytes.collect do |s|
267
244
  raise TSS::ArgumentError, 'invalid shares, no index' if s[0].blank?
268
245
  raise TSS::ArgumentError, 'invalid shares, zero index' if s[0] == 0
@@ -274,13 +251,13 @@ module TSS
274
251
  end
275
252
  end
276
253
 
277
- def share_combinations_mode_allowed?(hash_id)
254
+ def share_combinations_mode_allowed!(hash_id)
278
255
  unless Hasher.codes_without_none.include?(hash_id)
279
256
  raise TSS::ArgumentError, 'invalid options, combinations mode can only be used with hashed shares.'
280
257
  end
281
258
  end
282
259
 
283
- def share_combinations_out_of_bounds?(shares, threshold, max_combinations = 1_000_000)
260
+ def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000)
284
261
  # Raise if the number of combinations is too high.
285
262
  # If this is not checked, the number of combinations can quickly grow into
286
263
  # numbers that cannot be calculated before the end of the universe.
data/lib/tss/hasher.rb CHANGED
@@ -5,6 +5,10 @@ module TSS
5
5
  SHA256: { code: 2, bytesize: 32, hasher: Digest::SHA256 }
6
6
  }.freeze
7
7
 
8
+ # Lookup the Symbol key for a Hash with the code.
9
+ #
10
+ # @param code [Integer] the hash code to convert to a Symbol key
11
+ # @return [Symbol] the hash key Symbol
8
12
  def self.key_from_code(code)
9
13
  return nil unless Hasher.codes.include?(code)
10
14
  HASHES.each do |k, v|
@@ -12,40 +16,70 @@ module TSS
12
16
  end
13
17
  end
14
18
 
19
+ # Lookup the hash code for the hash matching hash_key.
20
+ #
21
+ # @param hash_key [Symbol, String] the hash key to convert to an Integer code
22
+ # @return [Integer] the hash key code
15
23
  def self.code(hash_key)
16
24
  HASHES[hash_key.upcase.to_sym][:code]
17
25
  end
18
26
 
19
- # All valid hash codes, including NONE
27
+ # Lookup all valid hash codes, including NONE.
28
+ #
29
+ # @return [Array<Integer>] all hash codes including NONE
20
30
  def self.codes
21
31
  HASHES.collect do |_k, v|
22
32
  v[:code]
23
33
  end
24
34
  end
25
35
 
26
- # All valid hash codes that actually do hashing
36
+ # All valid hash codes that actually do hashing, excluding NONE.
37
+ #
38
+ # @return [Array<Integer>] all hash codes excluding NONE
27
39
  def self.codes_without_none
28
40
  HASHES.collect do |_k, v|
29
41
  v[:code] if v[:code] > 0
30
42
  end.compact
31
43
  end
32
44
 
45
+ # Lookup the size in Bytes for a specific hash_key.
46
+ #
47
+ # @param hash_key [Symbol, String] the hash key to lookup
48
+ # @return [Integer] the size in Bytes for a specific hash_key
33
49
  def self.bytesize(hash_key)
34
50
  HASHES[hash_key.upcase.to_sym][:bytesize]
35
51
  end
36
52
 
53
+ # Return a hexdigest hash for a String using hash_key hash algorithm.
54
+ # Returns '' if hash_key == :NONE
55
+ #
56
+ # @param hash_key [Symbol, String] the hash key to use to hash a String
57
+ # @param str [String] the String to hash
58
+ # @return [String] the hex digest for str
37
59
  def self.hex_string(hash_key, str)
38
60
  hash_key = hash_key.upcase.to_sym
39
61
  return '' if hash_key == :NONE
40
62
  HASHES[hash_key][:hasher].send(:hexdigest, str)
41
63
  end
42
64
 
65
+ # Return a Byte String hash for a String using hash_key hash algorithm.
66
+ # Returns '' if hash_key == :NONE
67
+ #
68
+ # @param hash_key [Symbol, String] the hash key to use to hash a String
69
+ # @param str [String] the String to hash
70
+ # @return [String] the Byte String digest for str
43
71
  def self.byte_string(hash_key, str)
44
72
  hash_key = hash_key.upcase.to_sym
45
73
  return '' if hash_key == :NONE
46
74
  HASHES[hash_key][:hasher].send(:digest, str)
47
75
  end
48
76
 
77
+ # Return a Byte Array hash for a String using hash_key hash algorithm.
78
+ # Returns [] if hash_key == :NONE
79
+ #
80
+ # @param hash_key [Symbol, String] the hash key to use to hash a String
81
+ # @param str [String] the String to hash
82
+ # @return [Array<Integer>] the Byte Array digest for str
49
83
  def self.byte_array(hash_key, str)
50
84
  hash_key = hash_key.upcase.to_sym
51
85
  return [] if hash_key == :NONE
data/lib/tss/splitter.rb CHANGED
@@ -42,57 +42,33 @@ module TSS
42
42
  .constrained(lteq: 255)
43
43
  .default(0)
44
44
 
45
- # The `split` method takes a Hash of arguments. The following hash key args
46
- # may be passed. Only `secret:` is required and the rest will be set to
47
- # reasonable and secure defaults if unset. All args will be validated for
48
- # correct type and values on object instantiation.
45
+ # To split a secret into a set of shares, the following
46
+ # procedure, or any equivalent method, is used:
49
47
  #
50
- # `secret:` (required) takes a String (UTF-8 or US-ASCII encoding) with a
51
- # length between 1..65_534
48
+ # This operation takes an octet string S, whose length is L octets, and
49
+ # a threshold parameter M, and generates a set of N shares, any M of
50
+ # which can be used to reconstruct the secret.
52
51
  #
53
- # `threshold:` The number of shares (M) that will be required to recombine the
54
- # secret. Must be a value between 1..255 inclusive. Defaults to
55
- # a threshold of 3 shares.
52
+ # The secret S is treated as an unstructured sequence of octets. It is
53
+ # not expected to be null-terminated. The number of octets in the
54
+ # secret may be anywhere from zero up to 65,534 (that is, two less than
55
+ # 2^16).
56
56
  #
57
- # `num_shares:` The total number of shares (N) that will be created. Must be
58
- # a value between the `threshold` value (M) and 255 inclusive.
59
- # The upper limit is particular to the TSS algorithm used.
60
- # Defaults to generating 5 total shares.
57
+ # The threshold parameter M is the number of shares that will be needed
58
+ # to reconstruct the secret. This value may be any number between one
59
+ # and 255, inclusive.
61
60
  #
62
- # `identifier:` A 0-16 bytes String limited to the characters 0-9, a-z, A-Z,
63
- # the dash (-), the underscore (_), and the period (.). The identifier will
64
- # be embedded in each the binary header of each share and should not reveal
65
- # anything about the secret. It defaults to the value of `SecureRandom.hex(8)`
66
- # which returns a random 16 Byte string which represents a Base10 decimal
67
- # between 1 and 18446744073709552000.
61
+ # The number of shares N that will be generated MUST be between the
62
+ # threshold value M and 255, inclusive. The upper limit is particular
63
+ # to the TSS algorithm specified in this document.
68
64
  #
69
- # `hash_alg:` The one-way hash algorithm that will be used to verify the
70
- # secret returned by a later recombine operation is identical to what was
71
- # split. This value will be concatenated with the secret prior to splitting.
72
- # The valid hash algorithm values are `NONE`, `SHA1`, and `SHA256`. Defaults
73
- # to `SHA256`. The use of `NONE` is discouraged as it does not allow those
74
- # who are recombining the shares to verify if they have in fact recovered
75
- # the correct secret.
76
- #
77
- # `pad_blocksize:` An integer representing the nearest multiple of Bytes
78
- # to left pad the secret to. Defaults to not adding any padding (0). Padding
79
- # is done with the "\u001F" character (decimal 31 in a Byte Array).
80
- # Since TSS share data (minus the header) is essentially the same size as the
81
- # original secret, padding smaller secrets may help mask the size of the
82
- # contents from an attacker. Padding is not part of the RTSS spec so other
83
- # TSS clients won't strip off the padding and may not validate correctly.
84
- # If you need this interoperability you should probably pad the secret
85
- # yourself prior to splitting it and leave the default zero-length pad in
86
- # place. You would also need to manually remove the padding you added after
87
- # the share is recombined.
88
- #
89
- # Calling `split` *must* return an Array of formatted shares or raise one of
90
- # `TSS::Error` or `TSS::ArgumentError` exceptions if anything has gone wrong.
65
+ # If the operation can not be completed successfully, then an error
66
+ # code should be returned.
91
67
  #
92
68
  def split
93
- secret_has_acceptable_encoding(secret)
94
- secret_does_not_begin_with_padding_char(secret)
95
- num_shares_not_less_than_threshold(threshold, num_shares)
69
+ secret_has_acceptable_encoding!(secret)
70
+ secret_does_not_begin_with_padding_char!(secret)
71
+ num_shares_not_less_than_threshold!(threshold, num_shares)
96
72
 
97
73
  # RTSS : Combine the secret with a hash digest before splitting. On recombine
98
74
  # the two will be separated again and the hash used to validate the
@@ -102,7 +78,7 @@ module TSS
102
78
  hashed_secret = Hasher.byte_array(hash_alg, secret)
103
79
  secret_bytes = Util.utf8_to_bytes(padded_secret) + hashed_secret
104
80
 
105
- secret_bytes_is_smaller_than_max_size(secret_bytes)
81
+ secret_bytes_is_smaller_than_max_size!(secret_bytes)
106
82
 
107
83
  # For each share, a distinct Share Index is generated. Each Share
108
84
  # Index is an octet other than the all-zero octet. All of the Share
@@ -145,7 +121,7 @@ module TSS
145
121
  # build up a common binary struct header for all shares
146
122
  header = share_header(identifier, hash_alg, threshold, shares.first.length)
147
123
 
148
- # create each binary share and return it.
124
+ # create each binary or human share and return it.
149
125
  shares.map! do |s|
150
126
  binary = (header + s.pack('C*')).force_encoding('ASCII-8BIT')
151
127
  # join with URL safe '~'
@@ -156,25 +132,25 @@ module TSS
156
132
 
157
133
  private
158
134
 
159
- def secret_has_acceptable_encoding(secret)
135
+ def secret_has_acceptable_encoding!(secret)
160
136
  unless secret.encoding.name == 'UTF-8' || secret.encoding.name == 'US-ASCII'
161
137
  raise TSS::ArgumentError, "invalid secret, must be a UTF-8 or US-ASCII encoded String not '#{secret.encoding.name}'"
162
138
  end
163
139
  end
164
140
 
165
- def secret_does_not_begin_with_padding_char(secret)
141
+ def secret_does_not_begin_with_padding_char!(secret)
166
142
  if secret.slice(0) == "\u001F"
167
143
  raise TSS::ArgumentError, 'invalid secret, first byte of secret is the reserved left-pad character (\u001F)'
168
144
  end
169
145
  end
170
146
 
171
- def num_shares_not_less_than_threshold(threshold, num_shares)
147
+ def num_shares_not_less_than_threshold!(threshold, num_shares)
172
148
  if num_shares < threshold
173
149
  raise TSS::ArgumentError, "invalid num_shares, must be >= threshold (#{threshold})"
174
150
  end
175
151
  end
176
152
 
177
- def secret_bytes_is_smaller_than_max_size(secret_bytes)
153
+ def secret_bytes_is_smaller_than_max_size!(secret_bytes)
178
154
  if secret_bytes.size >= 65_535
179
155
  raise TSS::ArgumentError, 'invalid secret, combined padded secret and hash are too large'
180
156
  end
data/lib/tss/tss.rb CHANGED
@@ -1,15 +1,142 @@
1
+ require 'digest'
2
+ require 'base64'
3
+ require 'securerandom'
4
+ require 'binary_struct'
5
+ require 'dry-types'
6
+ require 'tss/blank'
7
+ require 'tss/version'
8
+ require 'tss/types'
9
+ require 'tss/util'
10
+ require 'tss/hasher'
11
+ require 'tss/splitter'
12
+ require 'tss/combiner'
13
+
14
+ # Threshold Secret Sharing
15
+ #
16
+ # @author Glenn Rempe <glenn@rempe.us>
1
17
  module TSS
2
- def self.split(args)
3
- unless args.is_a?(Hash) && args.key?(:secret)
4
- raise TSS::ArgumentError, 'TSS.split takes a Hash of arguments with at least a :secret key'
18
+ # An unexpected error has occurred.
19
+ class Error < StandardError; end
20
+
21
+ # An argument provided is of the wrong type, or has an invalid value.
22
+ class ArgumentError < TSS::Error; end
23
+
24
+ # A secret was attmepted to be recovered, but failed due to invalid shares.
25
+ class NoSecretError < TSS::Error; end
26
+
27
+ # A secret was attempted to be recovered, but failed due to an invalid verifier hash.
28
+ class InvalidSecretHashError < TSS::Error; end
29
+
30
+ # Threshold Secret Sharing (TSS) provides a way to generate N shares
31
+ # from a value, so that any M of those shares can be used to
32
+ # reconstruct the original value, but any M-1 shares provide no
33
+ # information about that value. This method can provide shared access
34
+ # control on key material and other secrets that must be strongly
35
+ # protected.
36
+ #
37
+ # @param [Hash] opts the options to create a message with.
38
+ # @option opts [String] :secret takes a String (UTF-8 or US-ASCII encoding) with a length between 1..65_534
39
+ # @option opts [String] :threshold (3) The number of shares (M) that will be required to recombine the
40
+ # secret. Must be a value between 1..255 inclusive. Defaults to a threshold of 3 shares.
41
+ # @option opts [String] :num_shares (5) The total number of shares (N) that will be created. Must be
42
+ # a value between the `threshold` value (M) and 255 inclusive.
43
+ # The upper limit is particular to the TSS algorithm used.
44
+ # @option opts [String] :identifier (SecureRandom.hex(8)) A 0-16 bytes String limited to the characters 0-9, a-z, A-Z,
45
+ # the dash (-), the underscore (_), and the period (.). The identifier will
46
+ # be embedded in each the binary header of each share and should not reveal
47
+ # anything about the secret.
48
+ #
49
+ # It defaults to the value of `SecureRandom.hex(8)`
50
+ # which returns a random 16 Byte string which represents a Base10 decimal
51
+ # between 1 and 18446744073709552000.
52
+ # @option opts [String] :hash_alg ('SHA256') The one-way hash algorithm that will be used to verify the
53
+ # secret returned by a later recombine operation is identical to what was
54
+ # split. This value will be concatenated with the secret prior to splitting.
55
+ #
56
+ # The valid hash algorithm values are `NONE`, `SHA1`, and `SHA256`. Defaults
57
+ # to `SHA256`. The use of `NONE` is discouraged as it does not allow those
58
+ # who are recombining the shares to verify if they have in fact recovered
59
+ # the correct secret.
60
+ # @option opts [String] :format ('binary') the format of the String share output, 'binary' or 'human'
61
+ # @option opts [String] :pad_blocksize (0) An integer representing the nearest multiple of Bytes
62
+ # to left pad the secret to. Defaults to not adding any padding (0). Padding
63
+ # is done with the "\u001F" character (decimal 31 in a Byte Array).
64
+ #
65
+ # Since TSS share data (minus the header) is essentially the same size as the
66
+ # original secret, padding smaller secrets may help mask the size of the
67
+ # contents from an attacker. Padding is not part of the RTSS spec so other
68
+ # TSS clients won't strip off the padding and may not validate correctly.
69
+ #
70
+ # If you need this interoperability you should probably pad the secret
71
+ # yourself prior to splitting it and leave the default zero-length pad in
72
+ # place. You would also need to manually remove the padding you added after
73
+ # the share is recombined, or instruct recipients to ignore it.
74
+ #
75
+ # @return [Array<String>] an Array of String shares
76
+ # @raise [TSS::ArgumentError] if the options Types or Values are invalid
77
+ def self.split(opts)
78
+ unless opts.is_a?(Hash) && opts.key?(:secret)
79
+ raise TSS::ArgumentError, 'TSS.split takes a Hash of options with at least a :secret key'
80
+ end
81
+
82
+ begin
83
+ TSS::Splitter.new(opts).split
84
+ rescue Dry::Types::ConstraintError => e
85
+ raise TSS::ArgumentError, e.message
5
86
  end
6
- TSS::Splitter.new(args).split
7
87
  end
8
88
 
9
- def self.combine(args)
10
- unless args.is_a?(Hash) && args.key?(:shares)
11
- raise TSS::ArgumentError, 'TSS.combine takes a Hash of arguments with at least a :shares key'
89
+ # The reconstruction, or combining, operation reconstructs the secret from a
90
+ # set of valid shares where the number of shares is >= the threshold when the
91
+ # secret was initially split. All options are provided in a single Hash:
92
+ #
93
+ # @param [Hash] opts the options to create a message with.
94
+ # @option opts [Array<String>] :shares an Array of String shares to try to recombine into a secret
95
+ # @option opts [String] :select_by ('first') the method to use for selecting
96
+ # shares from the Array if more then threshold shares are provided. Can be
97
+ # 'first', 'sample', or 'combinations'.
98
+ #
99
+ # If the number of shares provided as input to the secret
100
+ # reconstruction operation is greater than the threshold M, then M
101
+ # of those shares are selected for use in the operation. The method
102
+ # used to select the shares can be chosen using the following values:
103
+ #
104
+ # `first` : If X shares are required by the threshold and more than X
105
+ # shares are provided, then the first X shares in the Array of shares provided
106
+ # will be used. All others will be discarded and the operation will fail if
107
+ # those selected shares cannot recreate the secret.
108
+ #
109
+ # `sample` : If X shares are required by the threshold and more than X
110
+ # shares are provided, then X shares will be randomly selected from the Array
111
+ # of shares provided. All others will be discarded and the operation will
112
+ # fail if those selected shares cannot recreate the secret.
113
+ #
114
+ # `combinations` : If X shares are required, and more than X shares are
115
+ # provided, then all possible combinations of the threshold number of shares
116
+ # will be tried to see if the secret can be recreated.
117
+ # This flexibility comes with a cost. All combinations of `threshold` shares
118
+ # must be generated. Due to the math associated with combinations it is possible
119
+ # that the system would try to generate a number of combinations that could never
120
+ # be generated or processed in many times the life of the Universe. This option
121
+ # can only be used if the possible combinations for the number of shares and the
122
+ # threshold needed to reconstruct a secret result in a number of combinations
123
+ # that is small enough to have a chance at being processed. If the number
124
+ # of combinations will be too large then the an Exception will be raised before
125
+ # processing has started.
126
+ #
127
+ # @return [Hash] a Hash containing the ':secret' and other metadata
128
+ # @raise [TSS::NoSecretError] if the secret cannot be re-created from the shares provided
129
+ # @raise [TSS::InvalidSecretHashError] if the embedded hash of the secret does not match the hash of the recreated secret
130
+ # @raise [TSS::ArgumentError] if the options Types or Values are invalid
131
+ def self.combine(opts)
132
+ unless opts.is_a?(Hash) && opts.key?(:shares)
133
+ raise TSS::ArgumentError, 'TSS.combine takes a Hash of options with at least a :shares key'
134
+ end
135
+
136
+ begin
137
+ TSS::Combiner.new(opts).combine
138
+ rescue Dry::Types::ConstraintError => e
139
+ raise TSS::ArgumentError, e.message
12
140
  end
13
- TSS::Combiner.new(args).combine
14
141
  end
15
142
  end
data/lib/tss/util.rb CHANGED
@@ -1,6 +1,9 @@
1
- # Common utility and math functions
2
1
  module TSS
2
+ # Common utility, math, and conversion functions.
3
3
  module Util
4
+ # The regex to match against human style shares
5
+ HUMAN_SHARE_RE = /^tss~v1~*[a-zA-Z0-9\.\-\_]{0,16}~[0-9]{1,3}~([a-zA-Z0-9\-\_]+\={0,2})$/
6
+
4
7
  # The EXP table. The elements are to be read from top to
5
8
  # bottom and left to right. For example, EXP[0] is 0x01, EXP[8] is
6
9
  # 0x1a, and so on. Note that the EXP[255] entry is present only as a
@@ -75,14 +78,23 @@ module TSS
75
78
  103, 74, 237, 222, 197, 49, 254, 24,
76
79
  13, 99, 140, 128, 192, 247, 112, 7].freeze
77
80
 
81
+ # GF(256) Addition
78
82
  # The addition operation returns the Bitwise
79
83
  # Exclusive OR (XOR) of its operands.
84
+ #
85
+ # @param a [Integer] a single Integer
86
+ # @param b [Integer] a single Integer
87
+ # @return [Integer] a GF(256) SUM of a and b
80
88
  def self.gf256_add(a, b)
81
89
  a ^ b
82
90
  end
83
91
 
84
- # The subtraction operation is identical, because the field has
85
- # characteristic two.
92
+ # The subtraction operation is identical to GF(256) addition, because the
93
+ # field has characteristic two.
94
+ #
95
+ # @param a [Integer] a single Integer
96
+ # @param b [Integer] a single Integer
97
+ # @return [Integer] a GF(256) subtraction of a and b
86
98
  def self.gf256_sub(a, b)
87
99
  gf256_add(a, b)
88
100
  end
@@ -91,6 +103,10 @@ module TSS
91
103
  # proceeds as follows. If either X or Y is equal to 0x00, then the
92
104
  # operation returns 0x00. Otherwise, the value EXP[ (LOG[X] + LOG[Y])
93
105
  # modulo 255] is returned.
106
+ #
107
+ # @param x [Integer] a single Integer
108
+ # @param y [Integer] a single Integer
109
+ # @return [Integer] a GF(256) multiplication of x and y
94
110
  def self.gf256_mul(x, y)
95
111
  return 0 if x == 0 || y == 0
96
112
  EXP[(LOG[x] + LOG[y]) % 255]
@@ -101,16 +117,18 @@ module TSS
101
117
  # the operation returns 0x00. If Y is equal to 0x00, then the input is
102
118
  # invalid, and an error condition occurs. Otherwise, the value
103
119
  # EXP[(LOG[X] - LOG[Y]) modulo 255] is returned.
120
+ #
121
+ # @param x [Integer] a single Integer
122
+ # @param y [Integer] a single Integer
123
+ # @return [Integer] a GF(256) division of x divided by y
124
+ # @raise [TSS::Error] if an attempt to divide by zero is tried
104
125
  def self.gf256_div(x, y)
105
126
  return 0 if x == 0
106
127
  raise TSS::Error, 'divide by zero' if y == 0
107
128
  EXP[(LOG[x] - LOG[y]) % 255]
108
129
  end
109
130
 
110
- # Share generation Functions
111
- ############################
112
-
113
- # We first define how to share a single octet.
131
+ # Share generation Function
114
132
  #
115
133
  # The function f takes as input a single octet X that is not equal to
116
134
  # 0x00, and an array A of M octets, and returns a single octet. It is
@@ -124,6 +142,11 @@ module TSS
124
142
  # the successive values of X^i used in the computation of the function
125
143
  # f can be computed by multiplying a value by X once for each term in
126
144
  # the summation.
145
+ #
146
+ # @param x [Integer] a single Integer
147
+ # @param bytes [Array<Integer>] an Array of Integers
148
+ # @return [Integer] a single Integer
149
+ # @raise [TSS::Error] if the index value for the share is zero
127
150
  def self.f(x, bytes)
128
151
  raise TSS::Error, 'invalid share index value, cannot be 0' if x == 0
129
152
  y = 0
@@ -137,9 +160,8 @@ module TSS
137
160
  y
138
161
  end
139
162
 
140
- # Secret Reconstruction Functions
141
- # ###############################
142
-
163
+ # Secret Reconstruction Function
164
+ #
143
165
  # We define the function L_i (for i from 0 to M-1, inclusive) that
144
166
  # takes as input an array U of M pairwise distinct octets, and is
145
167
  # defined as
@@ -154,6 +176,9 @@ module TSS
154
176
  # expression is never equal to zero because U[i] is not equal to U[j]
155
177
  # whenever i is not equal to j.
156
178
  #
179
+ # @param i [Integer] a single Integer
180
+ # @param u [Array<Integer>] an Array of Integers
181
+ # @return [Integer] a single Integer
157
182
  def self.basis_poly(i, u)
158
183
  prod = 1
159
184
 
@@ -165,6 +190,8 @@ module TSS
165
190
  prod
166
191
  end
167
192
 
193
+ # Secret Reconstruction Function
194
+ #
168
195
  # We denote the interpolation function as I. This function takes as
169
196
  # input two arrays U and V, each consisting of M octets, and returns a
170
197
  # single octet; it is defined as:
@@ -172,6 +199,9 @@ module TSS
172
199
  # I(U, V) = GF_SUM L_i(U) (*) V[i].
173
200
  # i=0,M-1
174
201
  #
202
+ # @param u [Array<Integer>] an Array of Integers
203
+ # @param v [Array<Integer>] an Array of Integers
204
+ # @return [Integer] a single Integer
175
205
  def self.lagrange_interpolation(u, v)
176
206
  sum = 0
177
207
 
@@ -182,25 +212,37 @@ module TSS
182
212
  sum
183
213
  end
184
214
 
185
- # Conversion Functions
186
- ######################
187
-
188
- # String to Byte Array
215
+ # Convert a UTF-8 String to an Array of Bytes
216
+ #
217
+ # @param str [String] a UTF-8 String to convert
218
+ # @return [Array<Integer>] an Array of Integer Bytes
189
219
  def self.utf8_to_bytes(str)
190
220
  str.bytes.to_a
191
221
  end
192
222
 
193
- # Byte Array to String
223
+ # Convert an Array of Bytes to a UTF-8 String
224
+ #
225
+ # @param bytes [Array<Integer>] an Array of Bytes to convert
226
+ # @return [String] a UTF-8 String
194
227
  def self.bytes_to_utf8(bytes)
195
228
  bytes.pack('C*').force_encoding('utf-8')
196
229
  end
197
230
 
231
+ # Convert an Array of Bytes to a hex String
232
+ #
233
+ # @param bytes [Array<Integer>] an Array of Bytes to convert
234
+ # @return [String] a hex String
198
235
  def self.bytes_to_hex(bytes)
199
236
  hex = ''
200
237
  bytes.each { |b| hex += sprintf('%02x', b).upcase }
201
238
  hex
202
239
  end
203
240
 
241
+ # Convert a hex String to an Array of Bytes
242
+ #
243
+ # @param str [String] a hex String to convert
244
+ # @return [Array<Integer>] an Array of Integer Bytes
245
+ # @raise [TSS::Error] if the hex value is not an even length
204
246
  def self.hex_to_bytes(str)
205
247
  # clone so we don't destroy the original string passed in by slicing it.
206
248
  strc = str.clone
@@ -212,16 +254,28 @@ module TSS
212
254
  bytes
213
255
  end
214
256
 
257
+ # Convert a hex String to a UTF-8 String
258
+ #
259
+ # @param hex [String] a hex String to convert
260
+ # @return [String] a UTF-8 String
215
261
  def self.hex_to_utf8(hex)
216
262
  bytes_to_utf8(hex_to_bytes(hex))
217
263
  end
218
264
 
265
+ # Convert a UTF-8 String to a hex String
266
+ #
267
+ # @param str [String] a UTF-8 String to convert
268
+ # @return [String] a hex String
219
269
  def self.utf8_to_hex(str)
220
270
  bytes_to_hex(utf8_to_bytes(str))
221
271
  end
222
272
 
223
- # String Helpers
224
-
273
+ # Left pad a String with pad_char in multiples of byte_multiple
274
+ #
275
+ # @param byte_multiple [Integer] pad in blocks of this size
276
+ # @param input_string [String] the String to pad
277
+ # @param pad_char [String] the String to pad with
278
+ # @return [String] a padded String
225
279
  def self.left_pad(byte_multiple, input_string, pad_char = "\u001F")
226
280
  return input_string if byte_multiple == 0
227
281
  pad_length = byte_multiple - (input_string.length % byte_multiple)
@@ -229,16 +283,44 @@ module TSS
229
283
  (pad_char * pad_length) + input_string
230
284
  end
231
285
 
232
- # Binary Header Helpers
286
+ # Constant time string comparison.
287
+ # Extracted from Rack::Utils
288
+ # https://github.com/rack/rack/blob/master/lib/rack/utils.rb
289
+ #
290
+ # NOTE: the values compared should be of fixed length, such as strings
291
+ # that have already been processed by HMAC. This should not be used
292
+ # on variable length plaintext strings because it could leak length info
293
+ # via timing attacks. The user provided value should always be passed
294
+ # in as the second parameter so as not to leak info about the secret.
295
+ #
296
+ # @param a [String] the private value
297
+ # @param b [String] the user provided value
298
+ # @return [true, false] whether the strings match or not
299
+ def self.secure_compare(a, b)
300
+ return false unless a.bytesize == b.bytesize
301
+
302
+ l = a.unpack("C*")
303
+
304
+ r, i = 0, -1
305
+ b.each_byte { |v| r |= v ^ l[i+=1] }
306
+ r == 0
307
+ end
233
308
 
309
+ # Extract the header data from a binary share.
310
+ # Extra "\x00" padding in the identifier will be removed.
311
+ #
312
+ # @param share [String] a binary octet share
313
+ # @return [Hash] header attributes
234
314
  def self.extract_share_header(share)
235
315
  h = Splitter::SHARE_HEADER_STRUCT.decode(share)
236
316
  h[:identifier] = h[:identifier].delete("\x00")
237
317
  return h
238
318
  end
239
319
 
240
- # Math Helpers
241
-
320
+ # Calculate the factorial for an Integer.
321
+ #
322
+ # @param n [Integer] the Integer to calculate for
323
+ # @return [Integer] the factorial of n
242
324
  def self.factorial(n)
243
325
  (1..n).reduce(:*) || 1
244
326
  end
@@ -246,19 +328,25 @@ module TSS
246
328
  # Calculate the number of combinations possible
247
329
  # for a given number of shares and threshold.
248
330
  #
249
- # See : http://www.wolframalpha.com/input/?i=20+choose+5
250
- # See : http://www.mathsisfun.com/combinatorics/combinations-permutations-calculator.html (Set balls, 20, 5, no, no) == 15504
251
- # See : http://www.mathsisfun.com/combinatorics/combinations-permutations.html
252
- # See : https://jdanger.com/calculating-factorials-in-ruby.html
253
- # See : http://chriscontinanza.com/2010/10/29/Array.html
254
- # See : http://stackoverflow.com/questions/2434503/ruby-factorial-function
331
+ # * http://www.wolframalpha.com/input/?i=20+choose+5
332
+ # * http://www.mathsisfun.com/combinatorics/combinations-permutations-calculator.html (Set balls, 20, 5, no, no) == 15504
333
+ # * http://www.mathsisfun.com/combinatorics/combinations-permutations.html
334
+ # * https://jdanger.com/calculating-factorials-in-ruby.html
335
+ # * http://chriscontinanza.com/2010/10/29/Array.html
336
+ # * http://stackoverflow.com/questions/2434503/ruby-factorial-function
255
337
  #
256
- # n is the total number of shares
257
- # r is the threshold number of shares
338
+ # @param n [Integer] the total number of shares
339
+ # @param r [Integer] the threshold number of shares
340
+ # @return [Integer] the number of possible combinations
258
341
  def self.calc_combinations(n, r)
259
342
  factorial(n) / (factorial(r) * factorial(n - r))
260
343
  end
261
344
 
345
+ # Converts an Integer into a delimiter separated String.
346
+ #
347
+ # @param n [Integer] an Integer to convert
348
+ # @param delimiter [String] the String to delimit n in three Integer groups
349
+ # @return [String] the object converted into a comma separated String.
262
350
  def self.int_commas(n, delimiter = ',')
263
351
  n.to_s.reverse.gsub(%r{([0-9]{3}(?=([0-9])))}, "\\1#{delimiter}").reverse
264
352
  end
data/lib/tss/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TSS
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.1.1'.freeze
3
3
  end
data/lib/tss.rb CHANGED
@@ -1,14 +1 @@
1
- require 'digest'
2
- require 'base64'
3
- require 'securerandom'
4
- require 'binary_struct'
5
- require 'dry-types'
6
1
  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/tss.gemspec CHANGED
@@ -9,6 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Glenn Rempe']
10
10
  spec.email = ['glenn@rempe.us']
11
11
 
12
+ spec.required_ruby_version = '>= 2.1.0'
13
+
12
14
  cert = File.expand_path('~/.gem-certs/gem-private_key_grempe.pem')
13
15
  if File.exist?(cert)
14
16
  spec.signing_key = cert
data.tar.gz.sig CHANGED
@@ -1,4 +1,2 @@
1
- k@2&��atr�'������@:��J
2
- 􍉩
3
- Y�*�|�F�G�G�NA
4
- ����5u�g�����w�Td�~�q��<�E����}Y�Jr�Z�P��r<,��#�#ೲ���ņ��c�nӻn����b5��Hxw;�����V��q� '�ౝ���cW��#�Ea�*E<�U-V���~��A����Иj%�~H�I_���'D
1
+ ��!�x^��9�(`�eܝ!���W�YJ
2
+ �,�A
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Glenn Rempe
@@ -30,7 +30,7 @@ cert_chain:
30
30
  zieXiXZSAojfFx9g91fKdIrlPbInHU/BaCxXSLBwvOM0drE+c2ue9X8gB55XAhzX
31
31
  37oBiw==
32
32
  -----END CERTIFICATE-----
33
- date: 2016-04-12 00:00:00.000000000 Z
33
+ date: 2016-04-14 00:00:00.000000000 Z
34
34
  dependencies:
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: dry-types
@@ -171,6 +171,7 @@ files:
171
171
  - ".rubocop.yml"
172
172
  - ".ruby-version"
173
173
  - ".travis.yml"
174
+ - ".yardopts"
174
175
  - CODE_OF_CONDUCT.md
175
176
  - Gemfile
176
177
  - LICENSE.txt
@@ -186,7 +187,6 @@ files:
186
187
  - lib/tss/blank.rb
187
188
  - lib/tss/cli.rb
188
189
  - lib/tss/combiner.rb
189
- - lib/tss/errors.rb
190
190
  - lib/tss/hasher.rb
191
191
  - lib/tss/splitter.rb
192
192
  - lib/tss/tss.rb
@@ -206,7 +206,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
206
206
  requirements:
207
207
  - - ">="
208
208
  - !ruby/object:Gem::Version
209
- version: '0'
209
+ version: 2.1.0
210
210
  required_rubygems_version: !ruby/object:Gem::Requirement
211
211
  requirements:
212
212
  - - ">="
metadata.gz.sig CHANGED
Binary file
data/lib/tss/errors.rb DELETED
@@ -1,4 +0,0 @@
1
- module TSS
2
- class Error < RuntimeError; end
3
- class ArgumentError < Error; end
4
- end