tss 0.1.0 → 0.1.1
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.yardopts +1 -0
- data/README.md +114 -28
- data/lib/tss/combiner.rb +90 -113
- data/lib/tss/hasher.rb +36 -2
- data/lib/tss/splitter.rb +26 -50
- data/lib/tss/tss.rb +135 -8
- data/lib/tss/util.rb +116 -28
- data/lib/tss/version.rb +1 -1
- data/lib/tss.rb +0 -13
- data/tss.gemspec +2 -0
- data.tar.gz.sig +2 -4
- metadata +4 -4
- metadata.gz.sig +0 -0
- data/lib/tss/errors.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53a46eaf5b6ac5b816d288d0ed9379dc59679145
|
4
|
+
data.tar.gz: 19fa36c7c3ef3d6e42c5a69cbe8a227a4985dfb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
[](https://badge.fury.io/rb/tss)
|
3
4
|
[](https://travis-ci.org/grempe/tss-rb)
|
4
5
|
[](https://coveralls.io/github/grempe/tss-rb?branch=master)
|
5
6
|
[](https://codeclimate.com/github/grempe/tss-rb)
|
6
7
|
|
7
|
-
## WARNING :
|
8
|
+
## WARNING : BETA CODE
|
8
9
|
|
9
|
-
This code is
|
10
|
-
|
11
|
-
|
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
|
-
#
|
18
|
-
#
|
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
|
-
#
|
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
|
-
#
|
24
|
-
#
|
25
|
-
#
|
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
|
-
#
|
30
|
-
#
|
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
|
-
#
|
35
|
-
#
|
36
|
-
#
|
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
|
-
#
|
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
|
-
#
|
53
|
-
#
|
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
|
-
#
|
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'
|
92
|
-
if
|
93
|
-
shares
|
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
|
124
|
-
share_combinations_out_of_bounds
|
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
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
202
|
-
|
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
|
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
|
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
|
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
|
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
|
260
|
-
shares_have_same_bytesize
|
261
|
-
shares_have_expected_length
|
262
|
-
shares_meet_threshold_min
|
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
|
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
|
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
|
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
|
-
#
|
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
|
-
#
|
46
|
-
#
|
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
|
-
#
|
51
|
-
#
|
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
|
-
#
|
54
|
-
#
|
55
|
-
#
|
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
|
-
#
|
58
|
-
#
|
59
|
-
#
|
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
|
-
#
|
63
|
-
#
|
64
|
-
#
|
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
|
-
#
|
70
|
-
#
|
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
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
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
|
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
|
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
|
-
#
|
186
|
-
|
187
|
-
|
188
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
250
|
-
#
|
251
|
-
#
|
252
|
-
#
|
253
|
-
#
|
254
|
-
#
|
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
|
257
|
-
# r
|
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
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
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.
|
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-
|
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:
|
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