tss 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -4
- data/.coco.yml +7 -0
- data/.editorconfig +12 -0
- data/.hound.yml +10 -0
- data/.inch.yml +9 -0
- data/.rubocop.yml +129 -40
- data/.ruby-version +1 -1
- data/.travis.yml +4 -3
- data/CHANGELOG.md +22 -0
- data/README.md +218 -162
- data/RELEASE.md +105 -0
- data/Rakefile +9 -0
- data/bin/tss +4 -1
- data/lib/tss/cli_combine.rb +136 -0
- data/lib/tss/cli_common.rb +40 -0
- data/lib/tss/cli_split.rb +156 -0
- data/lib/tss/cli_version.rb +17 -0
- data/lib/tss/combiner.rb +156 -72
- data/lib/tss/hasher.rb +4 -2
- data/lib/tss/splitter.rb +71 -33
- data/lib/tss/tss.rb +4 -5
- data/lib/tss/util.rb +4 -12
- data/lib/tss/version.rb +1 -1
- data/tss.gemspec +7 -4
- data.tar.gz.sig +0 -0
- metadata +64 -14
- metadata.gz.sig +0 -0
- data/lib/tss/cli.rb +0 -107
- data/lib/tss/types.rb +0 -4
data/bin/tss
CHANGED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
# Command Line Interface (CLI)
|
4
|
+
# See also, `bin/tss` executable.
|
5
|
+
module TSS
|
6
|
+
class CLI < Thor
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read shares from'
|
10
|
+
method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the recovered secret to'
|
11
|
+
|
12
|
+
desc 'combine', 'Enter shares to recover a split secret'
|
13
|
+
|
14
|
+
long_desc <<-LONGDESC
|
15
|
+
`tss combine` will take as input a number of shares that were generated
|
16
|
+
using the `tss split` command. Shares can be provided
|
17
|
+
using one of three different input methods; STDIN, a path to a file,
|
18
|
+
or when prompted for them interactively.
|
19
|
+
|
20
|
+
You can enter shares one by one, or from a text file of shares. If the
|
21
|
+
shares are successfully combined to recover a secret, the secret and
|
22
|
+
some metadata will be written to STDOUT or a file.
|
23
|
+
|
24
|
+
Optional Params:
|
25
|
+
|
26
|
+
input_file :
|
27
|
+
Provide the path to a file containing shares. Any lines in the file not
|
28
|
+
beginning with `tss~` and matching the pattern expected for shares will be
|
29
|
+
ignored. Leading and trailing whitespace or any other text will be ignored
|
30
|
+
as long as the shares are each on a line by themselves.
|
31
|
+
|
32
|
+
output_file :
|
33
|
+
Provide the path to a file where you would like to write any recovered
|
34
|
+
secret, instead of to STDOUT. When this option is provided the output file
|
35
|
+
will contain only the secret itself. Some metadata, including the hash digest
|
36
|
+
of the secret, will be written to STDOUT. Running `sha1sum` or `sha256sum`
|
37
|
+
on the output file should provide a digest matching that of the secret
|
38
|
+
when it was originally split.
|
39
|
+
|
40
|
+
Example w/ options:
|
41
|
+
|
42
|
+
$ tss combine -I shares.txt -O secret.txt
|
43
|
+
LONGDESC
|
44
|
+
|
45
|
+
# rubocop:disable CyclomaticComplexity
|
46
|
+
def combine
|
47
|
+
log('Starting combine')
|
48
|
+
log("options : #{options.inspect}")
|
49
|
+
shares = []
|
50
|
+
|
51
|
+
# There are three ways to pass in shares. STDIN, by specifying
|
52
|
+
# `--input-file`, and in response to being prompted and entering shares
|
53
|
+
# line by line.
|
54
|
+
|
55
|
+
# STDIN
|
56
|
+
# Usage : echo 'foo bar baz' | bundle exec bin/tss split | bundle exec bin/tss combine
|
57
|
+
unless STDIN.tty?
|
58
|
+
$stdin.each_line do |line|
|
59
|
+
line = line.strip
|
60
|
+
exit_if_binary!(line)
|
61
|
+
|
62
|
+
if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
|
63
|
+
shares << line
|
64
|
+
else
|
65
|
+
log("Skipping invalid share file line : #{line}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Read from an Input File
|
71
|
+
if STDIN.tty? && options[:input_file]
|
72
|
+
log("Input file specified : #{options[:input_file]}")
|
73
|
+
|
74
|
+
if File.exist?(options[:input_file])
|
75
|
+
log("Input file found : #{options[:input_file]}")
|
76
|
+
|
77
|
+
file = File.open(options[:input_file], 'r')
|
78
|
+
while !file.eof?
|
79
|
+
line = file.readline.strip
|
80
|
+
exit_if_binary!(line)
|
81
|
+
|
82
|
+
if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
|
83
|
+
shares << line
|
84
|
+
else
|
85
|
+
log("Skipping invalid share file line : #{line}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
else
|
89
|
+
err("Filename '#{options[:input_file]}' does not exist.")
|
90
|
+
exit(1)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Enter shares in response to a prompt.
|
95
|
+
if STDIN.tty? && options[:input_file].blank?
|
96
|
+
say('Enter shares, one per line, and a dot (.) on a line by itself to finish :')
|
97
|
+
last_ans = nil
|
98
|
+
until last_ans == '.'
|
99
|
+
last_ans = ask('share> ').strip
|
100
|
+
exit_if_binary!(last_ans)
|
101
|
+
|
102
|
+
if last_ans != '.' && last_ans.start_with?('tss~') && last_ans.match(Util::HUMAN_SHARE_RE)
|
103
|
+
shares << last_ans
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
begin
|
109
|
+
sec = TSS.combine(shares: shares)
|
110
|
+
|
111
|
+
say('')
|
112
|
+
say('RECOVERED SECRET METADATA')
|
113
|
+
say('*************************')
|
114
|
+
say("hash : #{sec[:hash]}")
|
115
|
+
say("hash_alg : #{sec[:hash_alg]}")
|
116
|
+
say("identifier : #{sec[:identifier]}")
|
117
|
+
say("process_time : #{sec[:process_time]}ms")
|
118
|
+
say("threshold : #{sec[:threshold]}")
|
119
|
+
|
120
|
+
# Write the secret to a file or STDOUT. The hash of the file checked
|
121
|
+
# using sha1sum or sha256sum should match the hash of the original
|
122
|
+
# secret when it was split.
|
123
|
+
if options[:output_file].present?
|
124
|
+
say("secret file : [#{options[:output_file]}]")
|
125
|
+
File.open(options[:output_file], 'w'){ |somefile| somefile.puts sec[:secret] }
|
126
|
+
else
|
127
|
+
say('secret :')
|
128
|
+
say(sec[:secret])
|
129
|
+
end
|
130
|
+
rescue TSS::Error => e
|
131
|
+
err("#{e.class} : #{e.message}")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
# rubocop:enable CyclomaticComplexity
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
# Command Line Interface (CLI)
|
4
|
+
# See also, `bin/tss` executable.
|
5
|
+
module TSS
|
6
|
+
class CLI < Thor
|
7
|
+
|
8
|
+
class_option :verbose, :type => :boolean, :aliases => '-v', :desc => 'Display additional logging output'
|
9
|
+
|
10
|
+
no_commands do
|
11
|
+
# rubocop:disable CyclomaticComplexity
|
12
|
+
def exit_if_binary!(str)
|
13
|
+
str.each_byte { |c|
|
14
|
+
# OK, 9 (TAB), 10 (CR), 13 (LF), >=32 for normal ASCII
|
15
|
+
# Usage of anything other than 10, 13, and 32-126 ASCII decimal codes
|
16
|
+
# looks as though contents are binary and not standard text.
|
17
|
+
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) || c == 127
|
18
|
+
err('STDIN secret appears to contain binary data.')
|
19
|
+
exit(1)
|
20
|
+
end
|
21
|
+
}
|
22
|
+
|
23
|
+
unless ['UTF-8', 'US-ASCII'].include?(str.encoding.name)
|
24
|
+
err('STDIN secret has a non UTF-8 or US-ASCII encoding.')
|
25
|
+
exit(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
# rubocop:enable CyclomaticComplexity
|
29
|
+
|
30
|
+
def log(str)
|
31
|
+
say_status(:log, "#{Time.now.utc.iso8601} : #{str}", :white) if options[:verbose]
|
32
|
+
end
|
33
|
+
|
34
|
+
def err(str)
|
35
|
+
say_status(:error, "#{Time.now.utc.iso8601} : #{str}", :red)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
# Command Line Interface (CLI)
|
4
|
+
# See also, `bin/tss` executable.
|
5
|
+
module TSS
|
6
|
+
class CLI < Thor
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
method_option :threshold, :aliases => '-t', :banner => 'threshold', :type => :numeric, :desc => '# of shares, of total, required to reconstruct a secret'
|
10
|
+
method_option :num_shares, :aliases => '-n', :banner => 'num_shares', :type => :numeric, :desc => '# of shares total that will be generated'
|
11
|
+
method_option :identifier, :aliases => '-i', :banner => 'identifier', :type => :string, :desc => 'A unique identifier string, 0-16 Bytes, [a-zA-Z0-9.-_]'
|
12
|
+
method_option :hash_alg, :aliases => '-h', :banner => 'hash_alg', :type => :string, :desc => 'A hash type for verification, NONE, SHA1, SHA256'
|
13
|
+
method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'human', :desc => 'Share output format, binary or human'
|
14
|
+
method_option :pad_blocksize, :aliases => '-p', :banner => 'pad_blocksize', :type => :numeric, :desc => 'Block size # secrets will be left-padded to, 0-255'
|
15
|
+
method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read the secret from'
|
16
|
+
method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the shares to'
|
17
|
+
|
18
|
+
desc 'split', 'Split a secret into shares that can be used to re-create the secret'
|
19
|
+
|
20
|
+
long_desc <<-LONGDESC
|
21
|
+
`tss split` will generate a set of Threshold Secret Sharing shares from
|
22
|
+
a SECRET provided. A secret to be split can be provided using one of three
|
23
|
+
different input methods; STDIN, a path to a file, or when prompted
|
24
|
+
for it interactively. In all cases the secret should be UTF-8 or
|
25
|
+
US-ASCII encoded text and be no larger than 65,535 Bytes (including header
|
26
|
+
and hash verification bytes).
|
27
|
+
|
28
|
+
Optional Params:
|
29
|
+
|
30
|
+
num_shares :
|
31
|
+
The number of total shares that will be generated.
|
32
|
+
|
33
|
+
threshold :
|
34
|
+
The threshold is the number of shares required to
|
35
|
+
recreate a secret. This is always a subset of the total
|
36
|
+
shares.
|
37
|
+
|
38
|
+
identifier :
|
39
|
+
A unique identifier string that will be attached
|
40
|
+
to each share. It can be 0-16 Bytes long and use the
|
41
|
+
characters [a-zA-Z0-9.-_]
|
42
|
+
|
43
|
+
hash_alg :
|
44
|
+
One of NONE, SHA1, SHA256. The algorithm to use for a one-way hash of the secret that will be split along with the secret.
|
45
|
+
|
46
|
+
pad_blocksize :
|
47
|
+
An Integer, 0-255, that represents a multiple to which the secret will be padded. For example if pad_blocksize is set to 8, the secret 'abc' would be left-padded to '00000abc' (the padding char is not zero, that is just for illustration).
|
48
|
+
|
49
|
+
format :
|
50
|
+
Whether to output the shares as a binary octet string (RTSS), or as more human friendly URL safe Base 64 encoded text with some metadata.
|
51
|
+
|
52
|
+
input_file :
|
53
|
+
Provide the path to a file containing UTF-8 or US-ASCII text, the contents of which will be used as the secret.
|
54
|
+
|
55
|
+
output_file :
|
56
|
+
Provide the path to a file where you would like to write the shares, one per line, instead of to STDOUT.
|
57
|
+
|
58
|
+
Example w/ options:
|
59
|
+
|
60
|
+
$ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f human
|
61
|
+
|
62
|
+
Enter your secret:
|
63
|
+
|
64
|
+
secret > my secret
|
65
|
+
|
66
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEBQ-AQG3PuU4oT4qHOh2oJmu-vQwGE6O5hsGRBNtdAYauTIi7VoIdi5imWSrswDdRy
|
67
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADECM0OK5TSamH3nubH3FJ2EGZ4Yux4eQC-mvcYY85oOe6ae3kpvVXjuRUDU1m6sX20X
|
68
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEDb7yF4Vhr1JqNe2Nc8IXo98hmKAxsqC3c_Mn3r3t60NxQMC22ate51StDOM-BImch
|
69
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEEIXU0FajldnRtEQMLK-ZYMO2MRa0NmkBFfNAOx7olbgXLkVbP9txXMDsdokblVwke
|
70
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEFfYo7EcQUOpMH09Ggz_403rvy1r9_ckI_Pd_hm1tRxX8FfzEWyXMAoFCKTOfIKgMo
|
71
|
+
tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEGDSmh74Ng8WTziMGZXAm5XcpFLqDl2oP4MH24XhYf33IIg1WsPIyMAznI0DJUeLpN
|
72
|
+
LONGDESC
|
73
|
+
|
74
|
+
# rubocop:disable CyclomaticComplexity
|
75
|
+
def split
|
76
|
+
log('Starting split')
|
77
|
+
log('options : ' + options.inspect)
|
78
|
+
args = {}
|
79
|
+
|
80
|
+
# There are three ways to pass in the secret. STDIN, by specifying
|
81
|
+
# `--input-file`, and after being prompted and entering your secret
|
82
|
+
# line by line.
|
83
|
+
|
84
|
+
# STDIN
|
85
|
+
# Usage : echo 'foo bar baz' | bundle exec bin/tss split
|
86
|
+
unless STDIN.tty?
|
87
|
+
secret = $stdin.read
|
88
|
+
exit_if_binary!(secret)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Read from an Input File
|
92
|
+
if STDIN.tty? && options[:input_file].present?
|
93
|
+
log("Input file specified : #{options[:input_file]}")
|
94
|
+
|
95
|
+
if File.exist?(options[:input_file])
|
96
|
+
log("Input file found : #{options[:input_file]}")
|
97
|
+
secret = File.open(options[:input_file], 'r'){ |file| file.read }
|
98
|
+
exit_if_binary!(secret)
|
99
|
+
else
|
100
|
+
err("Filename '#{options[:input_file]}' does not exist.")
|
101
|
+
exit(1)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Enter a secret in response to a prompt.
|
106
|
+
if STDIN.tty? && options[:input_file].blank?
|
107
|
+
say('Enter your secret, enter a dot (.) on a line by itself to finish :')
|
108
|
+
last_ans = nil
|
109
|
+
secret = []
|
110
|
+
|
111
|
+
while last_ans != '.'
|
112
|
+
last_ans = ask('secret > ')
|
113
|
+
secret << last_ans unless last_ans == '.'
|
114
|
+
end
|
115
|
+
|
116
|
+
# Strip whitespace from the leading and trailing edge of the secret.
|
117
|
+
# Separate each line of the secret with newline, and add a trailing
|
118
|
+
# newline so the hash of a secret when it is created will match
|
119
|
+
# the hash of a file output when recombinging shares.
|
120
|
+
secret = secret.join("\n").strip + "\n"
|
121
|
+
exit_if_binary!(secret)
|
122
|
+
end
|
123
|
+
|
124
|
+
args[:secret] = secret
|
125
|
+
args[:threshold] = options[:threshold] if options[:threshold]
|
126
|
+
args[:num_shares] = options[:num_shares] if options[:num_shares]
|
127
|
+
args[:identifier] = options[:identifier] if options[:identifier]
|
128
|
+
args[:hash_alg] = options[:hash_alg] if options[:hash_alg]
|
129
|
+
args[:pad_blocksize] = options[:pad_blocksize] if options[:pad_blocksize]
|
130
|
+
args[:format] = options[:format] if options[:format]
|
131
|
+
|
132
|
+
begin
|
133
|
+
log("Calling : TSS.split(#{args.inspect})")
|
134
|
+
shares = TSS.split(args)
|
135
|
+
|
136
|
+
if options[:output_file].present?
|
137
|
+
file_header = "# THRESHOLD SECRET SHARING SHARES\n"
|
138
|
+
file_header << "# #{Time.now.utc.iso8601}\n"
|
139
|
+
file_header << "# https://github.com/grempe/tss-rb\n"
|
140
|
+
file_header << "\n\n"
|
141
|
+
|
142
|
+
File.open(options[:output_file], 'w') do |somefile|
|
143
|
+
somefile.puts file_header + shares.join("\n")
|
144
|
+
end
|
145
|
+
log("Process complete : Output file written : #{options[:output_file]}")
|
146
|
+
else
|
147
|
+
$stdout.puts shares.join("\n")
|
148
|
+
log('Process complete')
|
149
|
+
end
|
150
|
+
rescue TSS::Error => e
|
151
|
+
err("#{e.class} : #{e.message}")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
# rubocop:enable CyclomaticComplexity
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
# Command Line Interface (CLI)
|
4
|
+
# See also, `bin/tss` executable.
|
5
|
+
module TSS
|
6
|
+
class CLI < Thor
|
7
|
+
desc 'version', 'tss version'
|
8
|
+
|
9
|
+
long_desc <<-LONGDESC
|
10
|
+
Display the current version of TSS
|
11
|
+
LONGDESC
|
12
|
+
|
13
|
+
def version
|
14
|
+
say("TSS #{TSS::VERSION}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|