Skip to content

Commit

Permalink
CLI changes.
Browse files Browse the repository at this point in the history
- now ensures text-like input, rejects binary-like input
- allows STDIN of secrets and STDOUT of shares
- combines shares from STDIN and writes info header when writing shares to file
- common log and error writing to terminal
  • Loading branch information
grempe committed Apr 19, 2016
1 parent 6407a27 commit 6327645
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 29 deletions.
1 change: 1 addition & 0 deletions bin/tss
Expand Up @@ -3,6 +3,7 @@

require 'tss'
require 'tss/cli_version'
require 'tss/cli_common'
require 'tss/cli_split'
require 'tss/cli_combine'

Expand Down
49 changes: 41 additions & 8 deletions lib/tss/cli_combine.rb
Expand Up @@ -12,7 +12,10 @@ class CLI < Thor
desc 'combine', 'Enter shares to recover a split secret'

long_desc <<-LONGDESC
`tss combine` will take as input a number of shares that were generated using the `tss split` command.
`tss combine` will take as input a number of shares that were generated
using the `tss split` command. Shares can be provided
using one of three different input methods; STDIN, a path to a file,
or when prompted for them interactively.
You can enter shares one by one, or from a text file of shares. If the
shares are successfully combined to recover a secret, the secret and
Expand Down Expand Up @@ -40,32 +43,62 @@ class CLI < Thor
LONGDESC

def combine
log('Starting combine')
log("options : #{options.inspect}")
shares = []

# read and process shares from a file
if options[:input_file].present?
# There are three ways to pass in shares. STDIN, by specifying
# `--input-file`, and in response to being prompted and entering shares
# line by line.

# STDIN
# Usage : echo 'foo bar baz' | bundle exec bin/tss split | bundle exec bin/tss combine
unless STDIN.tty?
$stdin.each_line do |line|
line = line.strip
exit_if_binary!(line)

if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
shares << line
else
log("Skipping invalid share file line : #{line}")
end
end
end

# Read from an Input File
if STDIN.tty? && options[:input_file]
log("Input file specified : #{options[:input_file]}")

if File.exist?(options[:input_file])
log("Input file found : #{options[:input_file]}")

file = File.open(options[:input_file], 'r')
while !file.eof?
line = file.readline.strip
exit_if_binary!(line)

if line.present? && line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
if line.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
shares << line
else
log("Skipping invalid share file line : #{line}")
end
end
else
say("ERROR : Filename '#{options[:input_file]}' does not exist.")
err("Filename '#{options[:input_file]}' does not exist.")
exit(1)
end
end

if options[:input_file].blank?
# Enter shares in response to a prompt.
if STDIN.tty? && options[:input_file].blank?
say('Enter shares, one per line, and a dot (.) on a line by itself to finish :')
last_ans = nil
until last_ans == '.'
last_ans = ask('share> ').strip
exit_if_binary!(last_ans)

if last_ans.present? && last_ans != '.' && last_ans.start_with?('tss~') && last_ans.match(Util::HUMAN_SHARE_RE)
if last_ans != '.' && last_ans.start_with?('tss~') && last_ans.match(Util::HUMAN_SHARE_RE)
shares << last_ans
end
end
Expand Down Expand Up @@ -94,7 +127,7 @@ def combine
say(sec[:secret])
end
rescue TSS::Error => e
say("ERROR : #{e.class} : #{e.message}")
err("#{e.class} : #{e.message}")
end
end
end
Expand Down
38 changes: 38 additions & 0 deletions lib/tss/cli_common.rb
@@ -0,0 +1,38 @@
require 'thor'

# Command Line Interface (CLI)
# See also, `bin/tss` executable.
module TSS
class CLI < Thor

class_option :verbose, :type => :boolean, :aliases => '-v', :desc => 'Display additional logging output'

no_commands do
def exit_if_binary!(str)
str.each_byte { |c|
# OK, 9 (TAB), 10 (CR), 13 (LF), >=32 for normal ASCII
# Usage of anything other than 10, 13, and 32-126 ASCII decimal codes
# looks as though contents are binary and not standard text.
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) || c == 127
err("STDIN secret appears to contain binary data.")
exit(1)
end
}

unless ['UTF-8', 'US-ASCII'].include?(str.encoding.name)
err("STDIN secret has a non UTF-8 or US-ASCII encoding.")
exit(1)
end
end

def log(str)
say_status(:log, "#{Time.now.utc.iso8601} : #{str}", :white) if options[:verbose]
end

def err(str)
say_status(:error, "#{Time.now.utc.iso8601} : #{str}", :red)
end
end

end
end
69 changes: 48 additions & 21 deletions lib/tss/cli_split.rb
Expand Up @@ -15,14 +15,15 @@ class CLI < Thor
method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read the secret from'
method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the shares to'

desc 'split', 'Split a secret into shares'
desc 'split', 'Split a secret into shares that can be used to re-create the secret'

long_desc <<-LONGDESC
`tss split` will generate a set of Threshold Secret Sharing shares from
the SECRET provided. To protect your secret from being saved in your
shell history you will be prompted for it unless you are providing
the secret from an external file. You can enter as many lines
as you like within the limits of the max size for a secret.
a SECRET provided. A secret to be split can be provided using one of three
different input methods; STDIN, a path to a file, or when prompted
for it interactively. In all cases the secret should be UTF-8 or
US-ASCII encoded text and be no larger than 65,535 Bytes (including header
and hash verification bytes).
Optional Params:
Expand Down Expand Up @@ -71,18 +72,37 @@ class CLI < Thor
LONGDESC

def split
log('Starting split')
log('options : ' + options.inspect)
args = {}

# read and process a secret from a file
if options[:input_file].present?
# There are three ways to pass in the secret. STDIN, by specifying
# `--input-file`, and after being prompted and entering your secret
# line by line.

# STDIN
# Usage : echo 'foo bar baz' | bundle exec bin/tss split
unless STDIN.tty?
secret = $stdin.read
exit_if_binary!(secret)
end

# Read from an Input File
if STDIN.tty? && options[:input_file].present?
log("Input file specified : #{options[:input_file]}")

if File.exist?(options[:input_file])
log("Input file found : #{options[:input_file]}")
secret = File.open(options[:input_file], 'r'){ |file| file.read }
exit_if_binary!(secret)
else
say("ERROR : Filename '#{options[:input_file]}' does not exist.")
err("Filename '#{options[:input_file]}' does not exist.")
exit(1)
end
else
# read and process a secret, line by line, ending with a (.)
end

# Enter a secret in response to a prompt.
if STDIN.tty? && options[:input_file].blank?
say('Enter your secret, enter a dot (.) on a line by itself to finish :')
last_ans = nil
secret = []
Expand All @@ -92,14 +112,12 @@ def split
secret << last_ans unless last_ans == '.'
end

# Strip whitespace from the leading and trailing edge
# of the secret.
#
# Separate each line of the secret with newline, and
# also add a trailing newline so the hashes of the secret
# when split and then joined and placed in a file will
# also match.
# Strip whitespace from the leading and trailing edge of the secret.
# Separate each line of the secret with newline, and add a trailing
# newline so the hash of a secret when it is created will match
# the hash of a file output when recombinging shares.
secret = secret.join("\n").strip + "\n"
exit_if_binary!(secret)
end

args[:secret] = secret
Expand All @@ -111,16 +129,25 @@ def split
args[:format] = options[:format] if options[:format]

begin
log("Calling : TSS.split(#{args.inspect})")
shares = TSS.split(args)

# write the shares to a file or STDOUT
if options[:output_file].present?
File.open(options[:output_file], 'w'){ |somefile| somefile.puts shares.join("\n") }
file_header = "# THRESHOLD SECRET SHARING SHARES\n"
file_header << "# #{Time.now.utc.iso8601}\n"
file_header << "# https://github.com/grempe/tss-rb\n"
file_header << "\n\n"

File.open(options[:output_file], 'w') do |somefile|
somefile.puts file_header + shares.join("\n")
end
log("Process complete : Output file written : #{options[:output_file]}")
else
say(shares.join("\n"))
$stdout.puts shares.join("\n")
log('Process complete')
end
rescue TSS::Error => e
say("ERROR : #{e.class} : #{e.message}")
err("#{e.class} : #{e.message}")
end
end
end
Expand Down

0 comments on commit 6327645

Please sign in to comment.