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 +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
|
+
[![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 :
|
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