tss 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGELOG.md +6 -0
- data/README.md +11 -12
- data/lib/tss/combiner.rb +4 -3
- data/lib/tss/custom_contracts.rb +66 -0
- data/lib/tss/hasher.rb +17 -7
- data/lib/tss/splitter.rb +7 -40
- data/lib/tss/tss.rb +15 -25
- data/lib/tss/util.rb +15 -0
- data/lib/tss/version.rb +1 -1
- metadata +3 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e448b355558f4f493400bac55a51fd14a8c56bf3
|
4
|
+
data.tar.gz: 537024e64cf91c9394784d76d9df61347add2041
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b69d52a5340fda8078e46e1ed95dc4c5a6e4ffbb54b5ac0295dce9fc2633decacf61a7616dff3c838772e3bb9e97be110d6a819a07fbf04d7f4ef0e8cfac9491
|
7
|
+
data.tar.gz: 5cc4fa4acc4e920dda21772432512da25b46bf9a16ac960496e0eb8cd0c0b2a3b5da979e4863e5571d0dcaa3962202ee9ec8bc3c98eb589b05ef8e8f5e653a0b
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## v0.3.0 (9/24/2016)
|
4
|
+
|
5
|
+
* Breaking change, identifier cannot be an empty string
|
6
|
+
* Much greater coverage of functions with Contracts
|
7
|
+
* Related documentation updates
|
8
|
+
|
3
9
|
## v0.2.0 (9/23/2016)
|
4
10
|
|
5
11
|
* clone the shares object passed to TSS.combine to prevent modification
|
data/README.md
CHANGED
@@ -57,8 +57,8 @@ command line or Ruby, with the default `3 out of 5` secret sharing.
|
|
57
57
|
### Command Line Interface (CLI)
|
58
58
|
|
59
59
|
```sh
|
60
|
-
$ echo 'secret unicode characters ½ ♥ 💩' |
|
61
|
-
$
|
60
|
+
$ echo 'secret unicode characters ½ ♥ 💩' | tss split -O /tmp/shares.txt
|
61
|
+
$ tss combine -I /tmp/shares.txt
|
62
62
|
|
63
63
|
RECOVERED SECRET METADATA
|
64
64
|
*************************
|
@@ -253,7 +253,7 @@ cautioned that this method may leave command history which may contain your
|
|
253
253
|
secret.
|
254
254
|
|
255
255
|
```text
|
256
|
-
echo 'a secret' |
|
256
|
+
echo 'a secret' | tss split -O /tmp/shares.txt
|
257
257
|
```
|
258
258
|
|
259
259
|
**Example : `--input-file`**
|
@@ -263,7 +263,7 @@ cautioned that storing the secret on a filesystem may expose you to certain
|
|
263
263
|
attacks since it can be hard to fully erase files once written.
|
264
264
|
|
265
265
|
```text
|
266
|
-
$
|
266
|
+
$ tss split -I /tmp/secret.txt
|
267
267
|
tss~v1~3f784d9d04dff796~3~M2Y3ODRkOWQwNGRmZjc5NgIDACsBvAVAo1dH3QrsYl0S30j2VVTu6dZLRe9EEk6zLtPTwZNYk-t1e4SAA41D
|
268
268
|
tss~v1~3f784d9d04dff796~3~M2Y3ODRkOWQwNGRmZjc5NgIDACsCMfy6YlI-CQrd-l93Xtw8YqOWSP5ToolKTaZyO2fPYzceaaVmB30ycdVr
|
269
269
|
tss~v1~3f784d9d04dff796~3~M2Y3ODRkOWQwNGRmZjc5NgIDACsD4IDasmAapmVFkuYPMvuavCtXiJgPZsaBHglGm4FU_U2rGZzfapRk-ZNN
|
@@ -279,7 +279,7 @@ in the command shell history or in a file on a filesystem that is hard to
|
|
279
279
|
erase securely. It does not protect you from simple keylogger attacks though.
|
280
280
|
|
281
281
|
```text
|
282
|
-
$
|
282
|
+
$ tss split
|
283
283
|
Enter your secret, enter a dot (.) on a line by itself to finish :
|
284
284
|
secret > the vault password is:
|
285
285
|
secret > V0ulT!
|
@@ -306,7 +306,7 @@ Here are some simple examples of using each:
|
|
306
306
|
**Example : `STDIN`**
|
307
307
|
|
308
308
|
```text
|
309
|
-
$ echo 'a secret' |
|
309
|
+
$ echo 'a secret' | tss split | tss combine
|
310
310
|
|
311
311
|
RECOVERED SECRET METADATA
|
312
312
|
*************************
|
@@ -322,8 +322,8 @@ a secret
|
|
322
322
|
**Example : `--input-file`**
|
323
323
|
|
324
324
|
```text
|
325
|
-
$ echo 'a secret' |
|
326
|
-
$
|
325
|
+
$ echo 'a secret' | tss split -O /tmp/shares.txt
|
326
|
+
$ tss combine -I /tmp/shares.txt
|
327
327
|
|
328
328
|
RECOVERED SECRET METADATA
|
329
329
|
*************************
|
@@ -351,7 +351,7 @@ tss~v1~ae2983e30e0471fe~3~YWUyOTgzZTMwZTA0NzFmZQIDACoDsnUaZf94pMArKZL7jmavzk9Qa6
|
|
351
351
|
tss~v1~ae2983e30e0471fe~3~YWUyOTgzZTMwZTA0NzFmZQIDACoETCWwSGLHcutzGCLb1yOkH7i6MF7j2b0wr0ZsUh6LuBmx6FkN2MbTkTc=
|
352
352
|
tss~v1~ae2983e30e0471fe~3~YWUyOTgzZTMwZTA0NzFmZQIDACoFazXEwgGBilMwY7k7DtDR9qqG7mgigMJLmIVB70eAULfQJ89xbXbONF4=
|
353
353
|
|
354
|
-
$
|
354
|
+
$ tss combine
|
355
355
|
Enter shares, one per line, and a dot (.) on a line by itself to finish :
|
356
356
|
share> tss~v1~ae2983e30e0471fe~3~YWUyOTgzZTMwZTA0NzFmZQIDACoBRjAH7wA0ncxJr3GliBqKxedf3bGaQH6AscuLqxtrKxxtxuC7A7a5Sqk=
|
357
357
|
share> tss~v1~ae2983e30e0471fe~3~YWUyOTgzZTMwZTA0NzFmZQIDACoClWVu75w-XHhoUgkbV5XaJ11stZCB5Z8HTArpv4QsIYDBpNO9DHQTbQ4=
|
@@ -402,14 +402,13 @@ between `1-255` inclusive. The `num_shares` value must be
|
|
402
402
|
greater-than-or-equal-to the `threshold` value. If you don't pass in
|
403
403
|
these options they will be set to `threshold = 3` and `num_shares = 5` by default.
|
404
404
|
|
405
|
-
The `identifier` is a `
|
405
|
+
The `identifier` is a `1-16` Byte String that will be embedded in
|
406
406
|
each output share and should uniquely identify a secret. All shares output
|
407
407
|
from the same secret splitting operation will have the same `identifier`. This
|
408
408
|
value can be retrieved easily from a share header and should be assumed to be
|
409
409
|
known to shareholders. Nothing that leaks information about the secret should
|
410
410
|
be used as an `identifier`. If an `identifier` is not set, it will default
|
411
|
-
to the output of `SecureRandom.hex(8)` which is 8 random hex bytes
|
412
|
-
16 characters long.
|
411
|
+
to the output of `SecureRandom.hex(8)` which is 8 random hex bytes (16 characters).
|
413
412
|
|
414
413
|
The `hash_alg` is a String that represents which cryptographic one-way
|
415
414
|
hash function should be embedded in shares. The hash is used to verify
|
data/lib/tss/combiner.rb
CHANGED
@@ -53,6 +53,7 @@ module TSS
|
|
53
53
|
# The output string is returned (along with some metadata).
|
54
54
|
#
|
55
55
|
# rubocop:disable CyclomaticComplexity
|
56
|
+
Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], :identifier => TSS::IdentifierArg, :process_time => C::Num, :secret => TSS::SecretArg, :threshold => TSS::ThresholdArg})
|
56
57
|
def combine
|
57
58
|
# unwrap 'human' shares into binary shares
|
58
59
|
if all_shares_appear_human?(shares)
|
@@ -287,9 +288,9 @@ module TSS
|
|
287
288
|
Contract C::ArrayOf[String] => C::Bool
|
288
289
|
def validate_all_shares(shares)
|
289
290
|
if shares_have_valid_headers!(shares) &&
|
290
|
-
|
291
|
-
|
292
|
-
|
291
|
+
shares_have_same_bytesize!(shares) &&
|
292
|
+
shares_have_expected_length!(shares) &&
|
293
|
+
shares_meet_threshold_min!(shares)
|
293
294
|
return true
|
294
295
|
else
|
295
296
|
return false
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module TSS
|
2
|
+
|
3
|
+
# Custom Contracts
|
4
|
+
# See : https://egonschiele.github.io/contracts.ruby/
|
5
|
+
class SecretArg
|
6
|
+
def self.valid? val
|
7
|
+
val.present? && val.is_a?(String) &&
|
8
|
+
val.length.between?(1,65502) &&
|
9
|
+
['UTF-8', 'US-ASCII'].include?(val.encoding.name) &&
|
10
|
+
val.slice(0) != "\u001F"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.to_s
|
14
|
+
'must be a UTF-8 or US-ASCII String between 1 and 65,502 characters in length and must not begin with the padding char \u001F'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ThresholdArg
|
19
|
+
def self.valid? val
|
20
|
+
val.present? &&
|
21
|
+
val.is_a?(Integer) &&
|
22
|
+
val.between?(1,255)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.to_s
|
26
|
+
'must be an Integer between 1 and 255'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NumSharesArg
|
31
|
+
def self.valid? val
|
32
|
+
val.present? &&
|
33
|
+
val.is_a?(Integer) &&
|
34
|
+
val.between?(1,255)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.to_s
|
38
|
+
'must be an Integer between 1 and 255'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class IdentifierArg
|
43
|
+
def self.valid? val
|
44
|
+
val.present? &&
|
45
|
+
val.is_a?(String) &&
|
46
|
+
val.length.between?(1,16) &&
|
47
|
+
val =~ /^[a-zA-Z0-9\-\_\.]*$/i
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.to_s
|
51
|
+
'must be a String between 1 and 16 characters in length limited to [a-z, A-Z, -, _, .]'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class PadBlocksizeArg
|
56
|
+
def self.valid? val
|
57
|
+
val.present? &&
|
58
|
+
val.is_a?(Integer) &&
|
59
|
+
val.between?(0,255)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.to_s
|
63
|
+
'must be an Integer between 0 and 255'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/tss/hasher.rb
CHANGED
@@ -2,15 +2,18 @@ module TSS
|
|
2
2
|
# Hasher is responsible for managing access to the various one-way hash
|
3
3
|
# functions that can be used to validate a secret.
|
4
4
|
class Hasher
|
5
|
+
include Contracts::Core
|
6
|
+
C = Contracts
|
7
|
+
|
5
8
|
HASHES = { NONE: { code: 0, bytesize: 0, hasher: nil },
|
6
9
|
SHA1: { code: 1, bytesize: 20, hasher: Digest::SHA1 },
|
7
|
-
SHA256: { code: 2, bytesize: 32, hasher: Digest::SHA256 }
|
8
|
-
}.freeze
|
10
|
+
SHA256: { code: 2, bytesize: 32, hasher: Digest::SHA256 }}.freeze
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
# Lookup the Symbol key for a Hash with the code.
|
13
|
+
#
|
14
|
+
# @param code [Integer] the hash code to convert to a Symbol key
|
15
|
+
# @return [Symbol,nil] the hash key Symbol or nil if not found
|
16
|
+
Contract C::Int => C::Maybe[Symbol]
|
14
17
|
def self.key_from_code(code)
|
15
18
|
return nil unless Hasher.codes.include?(code)
|
16
19
|
HASHES.each do |k, v|
|
@@ -22,6 +25,7 @@ module TSS
|
|
22
25
|
#
|
23
26
|
# @param hash_key [Symbol, String] the hash key to convert to an Integer code
|
24
27
|
# @return [Integer] the hash key code
|
28
|
+
Contract C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256'], C::Enum[:NONE, :SHA1, :SHA256]] => C::Maybe[C::Int]
|
25
29
|
def self.code(hash_key)
|
26
30
|
HASHES[hash_key.upcase.to_sym][:code]
|
27
31
|
end
|
@@ -29,6 +33,7 @@ module TSS
|
|
29
33
|
# Lookup all valid hash codes, including NONE.
|
30
34
|
#
|
31
35
|
# @return [Array<Integer>] all hash codes including NONE
|
36
|
+
Contract C::None => C::ArrayOf[C::Int]
|
32
37
|
def self.codes
|
33
38
|
HASHES.map do |_k, v|
|
34
39
|
v[:code]
|
@@ -38,6 +43,7 @@ module TSS
|
|
38
43
|
# All valid hash codes that actually do hashing, excluding NONE.
|
39
44
|
#
|
40
45
|
# @return [Array<Integer>] all hash codes excluding NONE
|
46
|
+
Contract C::None => C::ArrayOf[C::Int]
|
41
47
|
def self.codes_without_none
|
42
48
|
HASHES.map do |_k, v|
|
43
49
|
v[:code] if v[:code] > 0
|
@@ -48,8 +54,9 @@ module TSS
|
|
48
54
|
#
|
49
55
|
# @param hash_key [Symbol, String] the hash key to lookup
|
50
56
|
# @return [Integer] the size in Bytes for a specific hash_key
|
57
|
+
Contract C::Or[C::Enum['NONE', 'SHA1', 'SHA256'], C::Enum[:NONE, :SHA1, :SHA256]] => C::Int
|
51
58
|
def self.bytesize(hash_key)
|
52
|
-
HASHES[hash_key.
|
59
|
+
HASHES[hash_key.to_sym][:bytesize]
|
53
60
|
end
|
54
61
|
|
55
62
|
# Return a hexdigest hash for a String using hash_key hash algorithm.
|
@@ -58,6 +65,7 @@ module TSS
|
|
58
65
|
# @param hash_key [Symbol, String] the hash key to use to hash a String
|
59
66
|
# @param str [String] the String to hash
|
60
67
|
# @return [String] the hex digest for str
|
68
|
+
Contract C::Or[C::Enum['NONE', 'SHA1', 'SHA256'], C::Enum[:NONE, :SHA1, :SHA256]], String => String
|
61
69
|
def self.hex_string(hash_key, str)
|
62
70
|
hash_key = hash_key.upcase.to_sym
|
63
71
|
return '' if hash_key == :NONE
|
@@ -70,6 +78,7 @@ module TSS
|
|
70
78
|
# @param hash_key [Symbol, String] the hash key to use to hash a String
|
71
79
|
# @param str [String] the String to hash
|
72
80
|
# @return [String] the Byte String digest for str
|
81
|
+
Contract C::Or[C::Enum['NONE', 'SHA1', 'SHA256'], C::Enum[:NONE, :SHA1, :SHA256]], String => String
|
73
82
|
def self.byte_string(hash_key, str)
|
74
83
|
hash_key = hash_key.upcase.to_sym
|
75
84
|
return '' if hash_key == :NONE
|
@@ -82,6 +91,7 @@ module TSS
|
|
82
91
|
# @param hash_key [Symbol, String] the hash key to use to hash a String
|
83
92
|
# @param str [String] the String to hash
|
84
93
|
# @return [Array<Integer>] the Byte Array digest for str
|
94
|
+
Contract C::Or[C::Enum['NONE', 'SHA1', 'SHA256'], C::Enum[:NONE, :SHA1, :SHA256]], String => C::ArrayOf[C::Int]
|
85
95
|
def self.byte_array(hash_key, str)
|
86
96
|
hash_key = hash_key.upcase.to_sym
|
87
97
|
return [] if hash_key == :NONE
|
data/lib/tss/splitter.rb
CHANGED
@@ -8,26 +8,15 @@ module TSS
|
|
8
8
|
|
9
9
|
attr_reader :secret, :threshold, :num_shares, :identifier, :hash_alg, :format, :pad_blocksize
|
10
10
|
|
11
|
-
Contract ({ :secret =>
|
11
|
+
Contract ({ :secret => TSS::SecretArg, :threshold => C::Maybe[TSS::ThresholdArg], :num_shares => C::Maybe[TSS::NumSharesArg], :identifier => C::Maybe[TSS::IdentifierArg], :hash_alg => C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], :format => C::Maybe[C::Enum['binary', 'human']], :pad_blocksize => C::Maybe[TSS::PadBlocksizeArg] }) => C::Any
|
12
12
|
def initialize(opts = {})
|
13
13
|
@secret = opts.fetch(:secret)
|
14
|
-
raise TSS::ArgumentError, 'Invalid secret length. Must be between 1 and 65502' unless @secret.size.between?(1,65502)
|
15
|
-
|
16
14
|
@threshold = opts.fetch(:threshold, 3)
|
17
|
-
raise TSS::ArgumentError, 'Invalid threshold size. Must be between 1 and 255' unless @threshold.between?(1,255)
|
18
|
-
|
19
15
|
@num_shares = opts.fetch(:num_shares, 5)
|
20
|
-
raise TSS::ArgumentError, 'Invalid num_shares size. Must be between 1 and 255' unless @num_shares.between?(1,255)
|
21
|
-
|
22
16
|
@identifier = opts.fetch(:identifier, SecureRandom.hex(8))
|
23
|
-
raise TSS::ArgumentError, 'Invalid identifier characters' unless @identifier =~ /^[a-zA-Z0-9\-\_\.]*$/i
|
24
|
-
raise TSS::ArgumentError, 'Invalid identifier size. Must be between 0 and 16' unless @identifier.size.between?(0,16)
|
25
|
-
|
26
17
|
@hash_alg = opts.fetch(:hash_alg, 'SHA256')
|
27
18
|
@format = opts.fetch(:format, 'human')
|
28
|
-
|
29
19
|
@pad_blocksize = opts.fetch(:pad_blocksize, 0)
|
30
|
-
raise TSS::ArgumentError, 'Invalid pad_blocksize size. Must be between 0 and 255' unless @pad_blocksize.between?(0,255)
|
31
20
|
end
|
32
21
|
|
33
22
|
SHARE_HEADER_STRUCT = BinaryStruct.new([
|
@@ -60,9 +49,8 @@ module TSS
|
|
60
49
|
# If the operation can not be completed successfully, then an error
|
61
50
|
# code should be returned.
|
62
51
|
#
|
52
|
+
Contract C::None => C::ArrayOf[String]
|
63
53
|
def split
|
64
|
-
secret_has_acceptable_encoding!(secret)
|
65
|
-
secret_does_not_begin_with_padding_char!(secret)
|
66
54
|
num_shares_not_less_than_threshold!(threshold, num_shares)
|
67
55
|
|
68
56
|
# RTSS : Combine the secret with a hash digest before splitting. On recombine
|
@@ -129,35 +117,11 @@ module TSS
|
|
129
117
|
human = ['tss', 'v1', identifier, threshold, Base64.urlsafe_encode64(binary)].join('~')
|
130
118
|
format == 'binary' ? binary : human
|
131
119
|
end
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
120
|
|
136
|
-
|
137
|
-
#
|
138
|
-
# @param secret [String] a secret String
|
139
|
-
# @return [true] returns true if acceptable encoding
|
140
|
-
# @raise [TSS::ArgumentError] if invalid
|
141
|
-
def secret_has_acceptable_encoding!(secret)
|
142
|
-
unless secret.encoding.name == 'UTF-8' || secret.encoding.name == 'US-ASCII'
|
143
|
-
raise TSS::ArgumentError, "invalid secret, must be a UTF-8 or US-ASCII encoded String not '#{secret.encoding.name}'"
|
144
|
-
else
|
145
|
-
return true
|
146
|
-
end
|
121
|
+
return shares
|
147
122
|
end
|
148
123
|
|
149
|
-
|
150
|
-
#
|
151
|
-
# @param secret [String] a secret String
|
152
|
-
# @return [true] returns true if String does not begin with padding character
|
153
|
-
# @raise [TSS::ArgumentError] if invalid
|
154
|
-
def secret_does_not_begin_with_padding_char!(secret)
|
155
|
-
if secret.slice(0) == "\u001F"
|
156
|
-
raise TSS::ArgumentError, 'invalid secret, first byte of secret is the reserved left-pad character (\u001F)'
|
157
|
-
else
|
158
|
-
return true
|
159
|
-
end
|
160
|
-
end
|
124
|
+
private
|
161
125
|
|
162
126
|
# The num_shares must be greater than or equal to the threshold or it is invalid.
|
163
127
|
#
|
@@ -165,6 +129,7 @@ module TSS
|
|
165
129
|
# @param num_shares [Integer] the num_shares value
|
166
130
|
# @return [true] returns true if num_shares is >= threshold
|
167
131
|
# @raise [TSS::ArgumentError] if invalid
|
132
|
+
Contract TSS::ThresholdArg, TSS::NumSharesArg => C::Bool
|
168
133
|
def num_shares_not_less_than_threshold!(threshold, num_shares)
|
169
134
|
if num_shares < threshold
|
170
135
|
raise TSS::ArgumentError, "invalid num_shares, must be >= threshold (#{threshold})"
|
@@ -179,6 +144,7 @@ module TSS
|
|
179
144
|
# @param secret_bytes [Array<Integer>] the Byte Array containing the secret
|
180
145
|
# @return [true] returns true if num_shares is >= threshold
|
181
146
|
# @raise [TSS::ArgumentError] if invalid
|
147
|
+
Contract C::ArrayOf[C::Int] => C::Bool
|
182
148
|
def secret_bytes_is_smaller_than_max_size!(secret_bytes)
|
183
149
|
if secret_bytes.size >= 65_535
|
184
150
|
raise TSS::ArgumentError, 'invalid secret, combined padded secret and hash are too large'
|
@@ -194,6 +160,7 @@ module TSS
|
|
194
160
|
# @param threshold [Integer] the threshold value
|
195
161
|
# @param share_len [Integer] the length of the share in Bytes
|
196
162
|
# @return [String] returns an octet String of Bytes containing the binary header
|
163
|
+
Contract TSS::IdentifierArg, C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], TSS::ThresholdArg, C::Int => String
|
197
164
|
def share_header(identifier, hash_alg, threshold, share_len)
|
198
165
|
SHARE_HEADER_STRUCT.encode(identifier: identifier,
|
199
166
|
hash_id: Hasher.code(hash_alg),
|
data/lib/tss/tss.rb
CHANGED
@@ -5,6 +5,7 @@ require 'binary_struct'
|
|
5
5
|
require 'contracts'
|
6
6
|
require 'tss/blank'
|
7
7
|
require 'tss/version'
|
8
|
+
require 'tss/custom_contracts'
|
8
9
|
require 'tss/util'
|
9
10
|
require 'tss/hasher'
|
10
11
|
require 'tss/splitter'
|
@@ -14,6 +15,9 @@ require 'tss/combiner'
|
|
14
15
|
#
|
15
16
|
# @author Glenn Rempe <glenn@rempe.us>
|
16
17
|
module TSS
|
18
|
+
include Contracts::Core
|
19
|
+
C = Contracts
|
20
|
+
|
17
21
|
# An unexpected error has occurred.
|
18
22
|
class Error < StandardError; end
|
19
23
|
|
@@ -35,17 +39,17 @@ module TSS
|
|
35
39
|
#
|
36
40
|
# @param [Hash] opts the options to create a message with.
|
37
41
|
# @option opts [String] :secret takes a String (UTF-8 or US-ASCII encoding) with a length between 1..65_534
|
38
|
-
# @option opts [
|
42
|
+
# @option opts [Integer] :threshold (3) The number of shares (M) that will be required to recombine the
|
39
43
|
# secret. Must be a value between 1..255 inclusive. Defaults to a threshold of 3 shares.
|
40
|
-
# @option opts [
|
44
|
+
# @option opts [Integer] :num_shares (5) The total number of shares (N) that will be created. Must be
|
41
45
|
# a value between the `threshold` value (M) and 255 inclusive.
|
42
46
|
# The upper limit is particular to the TSS algorithm used.
|
43
|
-
# @option opts [String] :identifier (SecureRandom.hex(8)) A
|
47
|
+
# @option opts [String] :identifier (SecureRandom.hex(8)) A 1-16 byte String limited to the characters 0-9, a-z, A-Z,
|
44
48
|
# the dash (-), the underscore (_), and the period (.). The identifier will
|
45
49
|
# be embedded in each the binary header of each share and should not reveal
|
46
50
|
# anything about the secret.
|
47
51
|
#
|
48
|
-
#
|
52
|
+
# If the arg is nil it defaults to the value of `SecureRandom.hex(8)`
|
49
53
|
# which returns a random 16 Byte string which represents a Base10 decimal
|
50
54
|
# between 1 and 18446744073709552000.
|
51
55
|
# @option opts [String] :hash_alg ('SHA256') The one-way hash algorithm that will be used to verify the
|
@@ -57,7 +61,7 @@ module TSS
|
|
57
61
|
# who are recombining the shares to verify if they have in fact recovered
|
58
62
|
# the correct secret.
|
59
63
|
# @option opts [String] :format ('binary') the format of the String share output, 'binary' or 'human'
|
60
|
-
# @option opts [
|
64
|
+
# @option opts [Integer] :pad_blocksize (0) An integer representing the nearest multiple of Bytes
|
61
65
|
# to left pad the secret to. Defaults to not adding any padding (0). Padding
|
62
66
|
# is done with the "\u001F" character (decimal 31 in a Byte Array).
|
63
67
|
#
|
@@ -72,17 +76,10 @@ module TSS
|
|
72
76
|
# the share is recombined, or instruct recipients to ignore it.
|
73
77
|
#
|
74
78
|
# @return [Array<String>] an Array of String shares
|
75
|
-
# @raise [TSS::ArgumentError] if the options Types or Values are invalid
|
79
|
+
# @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
|
80
|
+
Contract ({ :secret => TSS::SecretArg, :threshold => C::Maybe[TSS::ThresholdArg], :num_shares => C::Maybe[TSS::NumSharesArg], :identifier => C::Maybe[TSS::IdentifierArg], :hash_alg => C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], :format => C::Maybe[C::Enum['binary', 'human']], :pad_blocksize => C::Maybe[TSS::PadBlocksizeArg] }) => C::ArrayOf[String]
|
76
81
|
def self.split(opts)
|
77
|
-
|
78
|
-
raise TSS::ArgumentError, 'TSS.split takes a Hash of options with at least a :secret key'
|
79
|
-
end
|
80
|
-
|
81
|
-
begin
|
82
|
-
TSS::Splitter.new(opts).split
|
83
|
-
rescue ParamContractError => e
|
84
|
-
raise TSS::ArgumentError, e.message
|
85
|
-
end
|
82
|
+
TSS::Splitter.new(opts).split
|
86
83
|
end
|
87
84
|
|
88
85
|
# The reconstruction, or combining, operation reconstructs the secret from a
|
@@ -126,16 +123,9 @@ module TSS
|
|
126
123
|
# @return [Hash] a Hash containing the ':secret' and other metadata
|
127
124
|
# @raise [TSS::NoSecretError] if the secret cannot be re-created from the shares provided
|
128
125
|
# @raise [TSS::InvalidSecretHashError] if the embedded hash of the secret does not match the hash of the recreated secret
|
129
|
-
# @raise [TSS::ArgumentError] if the options Types or Values are invalid
|
126
|
+
# @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
|
127
|
+
Contract ({ :shares => C::ArrayOf[String], :select_by => C::Maybe[C::Enum['first', 'sample', 'combinations']] }) => ({ :hash => C::Maybe[String], :hash_alg => C::Enum['NONE', 'SHA1', 'SHA256'], :identifier => TSS::IdentifierArg, :process_time => C::Num, :secret => TSS::SecretArg, :threshold => TSS::ThresholdArg})
|
130
128
|
def self.combine(opts)
|
131
|
-
|
132
|
-
raise TSS::ArgumentError, 'TSS.combine takes a Hash of options with at least a :shares key'
|
133
|
-
end
|
134
|
-
|
135
|
-
begin
|
136
|
-
TSS::Combiner.new(opts).combine
|
137
|
-
rescue ParamContractError => e
|
138
|
-
raise TSS::ArgumentError, e.message
|
139
|
-
end
|
129
|
+
TSS::Combiner.new(opts).combine
|
140
130
|
end
|
141
131
|
end
|
data/lib/tss/util.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
module TSS
|
2
2
|
# Common utility, math, and conversion functions.
|
3
3
|
module Util
|
4
|
+
include Contracts::Core
|
5
|
+
C = Contracts
|
6
|
+
|
4
7
|
# The regex to match against human style shares
|
5
8
|
HUMAN_SHARE_RE = /^tss~v1~*[a-zA-Z0-9\.\-\_]{0,16}~[0-9]{1,3}~([a-zA-Z0-9\-\_]+\={0,2})$/
|
6
9
|
|
@@ -216,6 +219,7 @@ module TSS
|
|
216
219
|
#
|
217
220
|
# @param str [String] a UTF-8 String to convert
|
218
221
|
# @return [Array<Integer>] an Array of Integer Bytes
|
222
|
+
Contract String => C::ArrayOf[C::Int]
|
219
223
|
def self.utf8_to_bytes(str)
|
220
224
|
str.bytes.to_a
|
221
225
|
end
|
@@ -224,6 +228,7 @@ module TSS
|
|
224
228
|
#
|
225
229
|
# @param bytes [Array<Integer>] an Array of Bytes to convert
|
226
230
|
# @return [String] a UTF-8 String
|
231
|
+
Contract C::ArrayOf[C::Int] => String
|
227
232
|
def self.bytes_to_utf8(bytes)
|
228
233
|
bytes.pack('C*').force_encoding('utf-8')
|
229
234
|
end
|
@@ -232,6 +237,7 @@ module TSS
|
|
232
237
|
#
|
233
238
|
# @param bytes [Array<Integer>] an Array of Bytes to convert
|
234
239
|
# @return [String] a hex String
|
240
|
+
Contract C::ArrayOf[C::Int] => String
|
235
241
|
def self.bytes_to_hex(bytes)
|
236
242
|
hex = ''
|
237
243
|
bytes.each { |b| hex += sprintf('%02x', b) }
|
@@ -242,6 +248,7 @@ module TSS
|
|
242
248
|
#
|
243
249
|
# @param str [String] a hex String to convert
|
244
250
|
# @return [Array<Integer>] an Array of Integer Bytes
|
251
|
+
Contract String => C::ArrayOf[C::Int]
|
245
252
|
def self.hex_to_bytes(str)
|
246
253
|
[str].pack('H*').unpack('C*')
|
247
254
|
end
|
@@ -250,6 +257,7 @@ module TSS
|
|
250
257
|
#
|
251
258
|
# @param hex [String] a hex String to convert
|
252
259
|
# @return [String] a UTF-8 String
|
260
|
+
Contract String => String
|
253
261
|
def self.hex_to_utf8(hex)
|
254
262
|
bytes_to_utf8(hex_to_bytes(hex))
|
255
263
|
end
|
@@ -258,6 +266,7 @@ module TSS
|
|
258
266
|
#
|
259
267
|
# @param str [String] a UTF-8 String to convert
|
260
268
|
# @return [String] a hex String
|
269
|
+
Contract String => String
|
261
270
|
def self.utf8_to_hex(str)
|
262
271
|
bytes_to_hex(utf8_to_bytes(str))
|
263
272
|
end
|
@@ -268,6 +277,7 @@ module TSS
|
|
268
277
|
# @param input_string [String] the String to pad
|
269
278
|
# @param pad_char [String] the String to pad with
|
270
279
|
# @return [String] a padded String
|
280
|
+
Contract C::Int, String, String => String
|
271
281
|
def self.left_pad(byte_multiple, input_string, pad_char = "\u001F")
|
272
282
|
return input_string if byte_multiple == 0
|
273
283
|
pad_length = byte_multiple - (input_string.length % byte_multiple)
|
@@ -288,6 +298,7 @@ module TSS
|
|
288
298
|
# @param a [String] the private value
|
289
299
|
# @param b [String] the user provided value
|
290
300
|
# @return [true, false] whether the strings match or not
|
301
|
+
Contract String, String => C::Bool
|
291
302
|
def self.secure_compare(a, b)
|
292
303
|
return false unless a.bytesize == b.bytesize
|
293
304
|
|
@@ -303,6 +314,7 @@ module TSS
|
|
303
314
|
#
|
304
315
|
# @param share [String] a binary octet share
|
305
316
|
# @return [Hash] header attributes
|
317
|
+
Contract String => Hash
|
306
318
|
def self.extract_share_header(share)
|
307
319
|
h = Splitter::SHARE_HEADER_STRUCT.decode(share)
|
308
320
|
h[:identifier] = h[:identifier].delete("\x00")
|
@@ -313,6 +325,7 @@ module TSS
|
|
313
325
|
#
|
314
326
|
# @param n [Integer] the Integer to calculate for
|
315
327
|
# @return [Integer] the factorial of n
|
328
|
+
Contract C::Int => C::Int
|
316
329
|
def self.factorial(n)
|
317
330
|
(1..n).reduce(:*) || 1
|
318
331
|
end
|
@@ -330,6 +343,7 @@ module TSS
|
|
330
343
|
# @param n [Integer] the total number of shares
|
331
344
|
# @param r [Integer] the threshold number of shares
|
332
345
|
# @return [Integer] the number of possible combinations
|
346
|
+
Contract C::Int, C::Int => C::Int
|
333
347
|
def self.calc_combinations(n, r)
|
334
348
|
factorial(n) / (factorial(r) * factorial(n - r))
|
335
349
|
end
|
@@ -339,6 +353,7 @@ module TSS
|
|
339
353
|
# @param n [Integer] an Integer to convert
|
340
354
|
# @param delimiter [String] the String to delimit n in three Integer groups
|
341
355
|
# @return [String] the object converted into a comma separated String.
|
356
|
+
Contract C::Int, String => String
|
342
357
|
def self.int_commas(n, delimiter = ',')
|
343
358
|
n.to_s.reverse.gsub(%r{([0-9]{3}(?=([0-9])))}, "\\1#{delimiter}").reverse
|
344
359
|
end
|
data/lib/tss/version.rb
CHANGED
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.
|
4
|
+
version: 0.3.0
|
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-09-
|
33
|
+
date: 2016-09-24 00:00:00.000000000 Z
|
34
34
|
dependencies:
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: sysrandom
|
@@ -238,6 +238,7 @@ files:
|
|
238
238
|
- lib/tss/cli_split.rb
|
239
239
|
- lib/tss/cli_version.rb
|
240
240
|
- lib/tss/combiner.rb
|
241
|
+
- lib/tss/custom_contracts.rb
|
241
242
|
- lib/tss/hasher.rb
|
242
243
|
- lib/tss/splitter.rb
|
243
244
|
- lib/tss/tss.rb
|
metadata.gz.sig
CHANGED
Binary file
|