grempe/tss-rb

View on GitHub
lib/tss/cli_combine.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require 'thor'

module TSS
  class CLI < Thor
    include Thor::Actions

    method_option :input_file, :aliases => '-I', :banner => 'input_file', :type => :string, :desc => 'A filename to read shares from'
    method_option :output_file, :aliases => '-O', :banner => 'output_file', :type => :string, :desc => 'A filename to write the recovered secret to'
    method_option :padding, :type => :boolean, :default => true, :desc => 'Whether PKCS#7 padding is expected in the secret and should be removed'

    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. 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
      some metadata will be written to STDOUT or a file.

      Optional Params:

      input_file :
      Provide the path to a file containing shares. Any lines in the file not
      beginning with `tss~` and matching the pattern expected for shares will be
      ignored. Leading and trailing whitespace or any other text will be ignored
      as long as the shares are each on a line by themselves.

      output_file :
      Provide the path to a file where you would like to write any recovered
      secret, instead of to STDOUT. When this option is provided the output file
      will contain only the secret itself. Some metadata, including the hash digest
      of the secret, will be written to STDOUT. Running `sha1sum` or `sha256sum`
      on the output file should provide a digest matching that of the secret
      when it was originally split.

      padding/no-padding :
      Whether or not PKCS#7 padding should be removed from secret data. By
      default padding is applied to shared secrets when created. Turning this
      off may be helpful if you need to combine shares created with a third-party
      library.

      Example w/ options:

      $ tss combine -I shares.txt -O secret.txt
    LONGDESC

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

      # 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.start_with?('tss~') && line.match(Util::HUMAN_SHARE_RE)
               shares << line
             else
               log("Skipping invalid share file line : #{line}")
             end
          end
        else
          err("Filename '#{options[:input_file]}' does not exist.")
          exit(1)
        end
      end

      # 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 != '.' && last_ans.start_with?('tss~') && last_ans.match(Util::HUMAN_SHARE_RE)
            shares << last_ans
          end
        end
      end

      begin
        sec = TSS.combine(shares: shares, padding: options[:padding])

        say('')
        say('RECOVERED SECRET METADATA')
        say('*************************')
        say("hash : #{sec[:hash]}")
        say("hash_alg : #{sec[:hash_alg]}")
        say("identifier : #{sec[:identifier]}")
        say("process_time : #{sec[:process_time]}ms")
        say("threshold : #{sec[:threshold]}")

        # Write the secret to a file or STDOUT. The hash of the file checked
        # using sha1sum or sha256sum should match the hash of the original
        # secret when it was split.
        if options[:output_file].present?
          say("secret file : [#{options[:output_file]}]")
          File.open(options[:output_file], 'w'){ |somefile| somefile.puts sec[:secret] }
        else
          say('secret :')
          say(sec[:secret])
        end
      rescue TSS::Error => e
        err("#{e.class} : #{e.message}")
      end
    end
    # rubocop:enable CyclomaticComplexity
  end
end