grempe/tss-rb

View on GitHub
lib/tss/cli_split.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require 'thor'

module TSS
  class CLI < Thor
    include Thor::Actions

    method_option :threshold, :aliases => '-t', :banner => 'threshold', :type => :numeric, :desc => '# of shares, of total, required to reconstruct a secret'
    method_option :num_shares, :aliases => '-n', :banner => 'num_shares', :type => :numeric, :desc => '# of shares total that will be generated'
    method_option :identifier, :aliases => '-i', :banner => 'identifier', :type => :string, :desc => 'A unique identifier string, 0-16 Bytes, [a-zA-Z0-9.-_]'
    method_option :hash_alg, :aliases => '-h', :banner => 'hash_alg', :type => :string, :desc => 'A hash type for verification, NONE, SHA1, SHA256'
    method_option :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'HUMAN', :desc => 'Share output format, BINARY or HUMAN'
    method_option :padding, :type => :boolean, :default => true, :desc => 'Whether to apply PKCS#7 padding to the secret'
    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 that can be used to re-create the secret'

    long_desc <<-LONGDESC
      `tss split` will generate a set of Threshold Secret Sharing shares from
      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 #{TSS::MAX_UNPADDED_SECRET_SIZE}
      bytes.

      Optional Params:

      num_shares :
      The number of total shares that will be generated.

      threshold  :
      The threshold is the number of shares required to
      recreate a secret. This is always a subset of the total
      shares.

      identifier :
      A unique identifier string that will be attached
      to each share. It can be 0-16 Bytes long and use the
      characters [a-zA-Z0-9.-_]

      hash_alg :
      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.

      padding/no-padding :
      Whether to apply PKCS#7 padding to secret. By default padding is applied. Turning this off may be helpful if you need to interoperate with a third party library.

      format :
      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.

      input_file :
      Provide the path to a file containing UTF-8 or US-ASCII text, the contents of which will be used as the secret.

      output_file :
      Provide the path to a file where you would like to write the shares, one per line, instead of to STDOUT.

      Example w/ options:

      $ tss split -t 3 -n 6 -i abc123 -h SHA256 -f HUMAN

      Enter your secret:

      secret >  my secret

      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEB113xpF37jGHm5QGhXKD8mgK2897MIQkSWri6ksNnAODn0efXznuBsSUnhlDIqQFU
      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEC4tZegQrC3z6-02er3FZaWMadtlvxPb1EI_FNjG0dFrcdEDj4V7Cmcw___SesJHHP
      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEDWPKPVjJaITosPGAMhvCgxCBB9uptl2h5UPngnw71V7Z9T-pnxiLKIfgUbRqyBrv-
      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEExY3ti8ckAIQC02OKCrpEVVnUmyg3NXO9oG3PNw3PlgbbKdFRi9gBCNN_tjkhT3An
      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEFf6k8XP-8_oCQPGQtUBy-yb8I25mrn6aA02ViJG4n1we7dgPOGkptWiSUJgQ_bboW
      tss~v1~abc123~3~YWJjMTIzAAAAAAAAAAAAAAIDADEGSiKTeaiFrd_ICgIn0OoYC3sjnhyWgxLWqiyVOsBdwVBBt9zhg4FKmA5MXXNb4MqN

    LONGDESC

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

      # 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
          err("Filename '#{options[:input_file]}' does not exist.")
          exit(1)
        end
      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 = []

        while last_ans != '.'
          last_ans = ask('secret > ')
          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 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
      args[:threshold]     = options[:threshold]     if options[:threshold]
      args[:num_shares]    = options[:num_shares]    if options[:num_shares]
      args[:identifier]    = options[:identifier]    if options[:identifier]
      args[:hash_alg]      = options[:hash_alg]      if options[:hash_alg]
      args[:format]        = options[:format]        if options[:format]
      args[:padding]       = options[:padding]

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

        if options[:output_file].present?
          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
          $stdout.puts shares.join("\n")
          log('Process complete')
        end
      rescue TSS::Error => e
        err("#{e.class} : #{e.message}")
      end
    end
    # rubocop:enable CyclomaticComplexity
  end
end