Skip to content

Commit

Permalink
Deeper Contracts integration, use yard-contracts for docs. Upcase Str…
Browse files Browse the repository at this point in the history
…ing args.
  • Loading branch information
grempe committed Sep 24, 2016
1 parent 5419c85 commit c4fbcc5
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 189 deletions.
2 changes: 1 addition & 1 deletion .yardopts
@@ -1 +1 @@
--no-private lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md
--plugin contracts -e lib/tss/custom_contracts.rb --no-private lib/**/*.rb - README.md LICENSE.txt CODE_OF_CONDUCT.md
18 changes: 9 additions & 9 deletions README.md
Expand Up @@ -296,7 +296,7 @@ tss~v1~546604c0b5e9138b~3~NTQ2NjA0YzBiNWU5MTM4YgIDAD8FqmJGRCiXokksUc3E1-gQNuNf2l
You can use the CLI to enter shares in order to recover a secret. Of course
you will need at least the number of shares necessary as determined
by the threshold when your shares were created. The `threshold` is visible
as the third field in every `human` formatted share.
as the third field in every `HUMAN` formatted share.

As with splitting a secret, there are also three methods of getting the shares
into the CLI. `STDIN`, a path to a file containing shares, or interactively.
Expand Down Expand Up @@ -419,12 +419,12 @@ and then combined with it prior to secret splitting. This means that the hash
is protected the same way as the secret. The algorithm used is
`secret || hash(secret)`. You can use one of `NONE`, `SHA1`, or `SHA256`.

The `format` arg takes a String Enum with either `'human'` (default) or
`'binary'` values. This instructs the output of a split to either provide an
The `format` arg takes an uppercase String Enum with either `'HUMAN'` (default) or
`'BINARY'` values. This instructs the output of a split to either provide an
array of binary octet strings (a standard RTSS format for interoperability), or
a human friendly URL Safe Base 64 encoded version of that same binary output.
The `human` format can be easily shared in a tweet, email, or even a URL. The
`human` format is prefixed with `tss~VERSION~IDENTIFIER~THRESHOLD~` to make it
The `HUMAN` format can be easily shared in a tweet, email, or even a URL. The
`HUMAN` format is prefixed with `tss~VERSION~IDENTIFIER~THRESHOLD~` to make it
easier to visually compare shares and see if they have matching identifiers and
if you have enough shares to reach the threshold.

Expand Down Expand Up @@ -523,23 +523,23 @@ of those shares are selected for use in the operation. The method
used to select the shares can be chosen with the `select_by:` argument
which takes the following values as options:

`select_by: 'first'` : If X shares are required by the threshold and more than X
`select_by: 'FIRST'` : If X shares are required by the threshold and more than X
shares are provided, then the first X shares in the Array of shares provided
will be used. All others will be discarded and the operation will fail if
those selected shares cannot recreate the secret.

`select_by: 'sample'` : If X shares are required by the threshold and more than X
`select_by: 'SAMPLE'` : If X shares are required by the threshold and more than X
shares are provided, then X shares will be randomly selected from the Array
of shares provided. All others will be discarded and the operation will
fail if those selected shares cannot recreate the secret.

`select_by: 'combinations'` : If X shares are required, and more than X shares are
`select_by: 'COMBINATIONS'` : If X shares are required, and more than X shares are
provided, then all possible combinations of the threshold number of shares
will be tried to see if the secret can be recreated.

**Warning**

This `combinations` flexibility comes with a cost. All combinations of
This `COMBINATIONS` flexibility comes with a cost. All combinations of
`threshold` shares must be generated before processing. Due to the math
associated with combinations it is possible that the system would try to
generate a number of combinations that could never be generated or processed
Expand Down
4 changes: 2 additions & 2 deletions lib/tss/cli_split.rb
Expand Up @@ -10,7 +10,7 @@ class CLI < Thor
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 :format, :aliases => '-f', :banner => 'format', :type => :string, :default => 'HUMAN', :desc => 'Share output format, BINARY or HUMAN'
method_option :pad_blocksize, :aliases => '-p', :banner => 'pad_blocksize', :type => :numeric, :desc => 'Block size # secrets will be left-padded to, 0-255'
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'
Expand Down Expand Up @@ -57,7 +57,7 @@ class CLI < Thor
Example w/ options:
$ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f human
$ tss split -t 3 -n 6 -i abc123 -h SHA256 -p 8 -f HUMAN
Enter your secret:
Expand Down
101 changes: 56 additions & 45 deletions lib/tss/combiner.rb
@@ -1,5 +1,8 @@
module TSS
# Combiner has responsibility for combining an Array of String shares back
# Warning, you probably don't want to use this directly. Instead
# see the TSS module.
#
# TSS::Combiner has responsibility for combining an Array of String shares back
# into the original secret the shares were split from. It is also responsible
# for doing extensive validation of user provided shares and ensuring
# that any recovered secret matches the hash of the original secret.
Expand All @@ -11,15 +14,17 @@ class Combiner

attr_reader :shares, :select_by

Contract ({ :shares => C::ArrayOf[String], :select_by => C::Maybe[C::Enum['first', 'sample', 'combinations']] }) => C::Any
Contract ({ :shares => C::ArrayOfShares, :select_by => C::Maybe[C::SelectByArg] }) => C::Any
def initialize(opts = {})
# clone the incoming shares so the object passed to this
# function doesn't get modified.
@shares = opts.fetch(:shares).clone
raise TSS::ArgumentError, 'Invalid number of shares. Must be between 1 and 255' unless @shares.size.between?(1,255)
@select_by = opts.fetch(:select_by, 'first')
@select_by = opts.fetch(:select_by, 'FIRST')
end

# Warning, you probably don't want to use this directly. Instead
# see the TSS module.
#
# To reconstruct a secret from a set of shares, the following
# procedure, or any equivalent method, is used:
#
Expand Down Expand Up @@ -52,8 +57,11 @@ def initialize(opts = {})
#
# The output string is returned (along with some metadata).
#
#
# @return an Hash of combined secret attributes
# @raise [ParamContractError, TSS::ArgumentError] if the options Types or Values are invalid
# rubocop:disable CyclomaticComplexity
Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C::Maybe[C::Enum['NONE', 'SHA1', 'SHA256']], :identifier => TSS::IdentifierArg, :process_time => C::Num, :secret => TSS::SecretArg, :threshold => TSS::ThresholdArg})
Contract C::None => ({ :hash => C::Maybe[String], :hash_alg => C::HashAlgArg, :identifier => C::IdentifierArg, :process_time => C::Num, :secret => C::SecretArg, :threshold => C::ThresholdArg})
def combine
# unwrap 'human' shares into binary shares
if all_shares_appear_human?(shares)
Expand All @@ -70,9 +78,9 @@ def combine

# Select a subset of the shares provided using the chosen selection
# method. If there are exactly the right amount of shares this is a no-op.
if select_by == 'first'
if select_by == 'FIRST'
@shares = shares.shift(threshold)
elsif select_by == 'sample'
elsif select_by == 'SAMPLE'
@shares = shares.sample(threshold)
end

Expand All @@ -85,7 +93,7 @@ def combine

shares_bytes_have_valid_indexes!(shares_bytes)

if select_by == 'combinations'
if select_by == 'COMBINATIONS'
share_combinations_mode_allowed!(hash_id)
share_combinations_out_of_bounds!(shares, threshold)

Expand All @@ -107,7 +115,7 @@ def combine
# Return a Hash with the secret and metadata
{
hash: secret[:hash],
hash_alg: secret[:hash_alg].to_s,
hash_alg: secret[:hash_alg],
identifier: identifier,
process_time: ((Time.now - start_processing_time)*1000).round(2),
secret: Util.bytes_to_utf8(secret[:secret]),
Expand All @@ -122,12 +130,12 @@ def combine
# and validate it against any one-way hash that was embedded in the shares
# along with the secret.
#
# @param hash_id [Integer] the ID of the one-way hash function to test with
# @param shares_bytes [Array<Array>] the shares as Byte Arrays to be evaluated
# @return [Array<Integer>] returns the secret as an Array of Bytes if it was recovered from the shares and validated
# @param hash_id the ID of the one-way hash function to test with
# @param shares_bytes the shares as Byte Arrays to be evaluated
# @return returns the secret as an Array of Bytes if it was recovered from the shares and validated
# @raise [TSS::NoSecretError] if the secret was not able to be recovered (with no hash)
# @raise [TSS::InvalidSecretHashError] if the secret was able to be recovered but the hash test failed
Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C::Enum[:NONE, :SHA1, :SHA256] })
Contract C::Int, C::ArrayOf[C::ArrayOf[C::Num]] => ({ :secret => C::ArrayOf[C::Num], :hash => C::Maybe[String], :hash_alg => C::HashAlgArg })
def extract_secret_from_shares!(hash_id, shares_bytes)
secret = []

Expand Down Expand Up @@ -172,17 +180,19 @@ def extract_secret_from_shares!(hash_id, shares_bytes)

# Strip off leading padding chars ("\u001F", decimal 31)
#
# @param secret [Array<Integer>] the secret to be stripped
# @return [Array<Integer>,nil] returns the secret, stripped of the leading padding char
# @param secret the secret to be stripped
# @return returns the secret, stripped of the leading padding char
# @raise [ParamContractError] if secret appears invalid
Contract C::ArrayOf[C::Num] => C::Maybe[Array]
def strip_left_pad(secret)
secret.shift while secret.first == 31
end

# Do all of the shares match the pattern expected of human style shares?
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true,false] returns true if all shares match the patterns, false if not
# @param shares the shares to be evaluated
# @return returns true if all shares match the patterns, false if not
# @raise [ParamContractError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def all_shares_appear_human?(shares)
shares.all? do |s|
Expand All @@ -194,9 +204,9 @@ def all_shares_appear_human?(shares)

# Convert an Array of human style shares to binary style
#
# @param shares [Array<String>] the shares to be converted
# @return [Array<String>] returns an Array of String shares in binary octet String format
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares the shares to be converted
# @return returns an Array of String shares in binary octet String format
# @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
Contract C::ArrayOf[String] => C::ArrayOf[String]
def convert_shares_human_to_binary(shares)
shares.map do |s|
Expand All @@ -216,9 +226,9 @@ def convert_shares_human_to_binary(shares)

# Do all shares have a common Byte size? They are invalid if not.
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true] returns true if all shares have the same Byte size
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares the shares to be evaluated
# @return returns true if all shares have the same Byte size
# @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def shares_have_same_bytesize!(shares)
shares.each do |s|
Expand All @@ -231,9 +241,9 @@ def shares_have_same_bytesize!(shares)

# Do all shares have a valid header and match each other? They are invalid if not.
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true] returns true if all shares have the same header
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares the shares to be evaluated
# @return returns true if all shares have the same header
# @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def shares_have_valid_headers!(shares)
fh = Util.extract_share_header(shares.first)
Expand All @@ -253,9 +263,9 @@ def shares_have_valid_headers!(shares)

# Do all shares have a the expected length? They are invalid if not.
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true] returns true if all shares have the same header
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares the shares to be evaluated
# @return returns true if all shares have the same header
# @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def shares_have_expected_length!(shares)
shares.each do |s|
Expand All @@ -268,9 +278,9 @@ def shares_have_expected_length!(shares)

# Were enough shares provided to meet the threshold? They are invalid if not.
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true] returns true if there are enough shares
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares the shares to be evaluated
# @return returns true if there are enough shares
# @raise [ParamContractError, TSS::ArgumentError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def shares_meet_threshold_min!(shares)
fh = Util.extract_share_header(shares.first)
Expand All @@ -283,8 +293,9 @@ def shares_meet_threshold_min!(shares)

# Were enough shares provided to meet the threshold? They are invalid if not.
#
# @param shares [Array<String>] the shares to be evaluated
# @return [true] returns true if all tests pass
# @param shares the shares to be evaluated
# @return returns true if all tests pass
# @raise [ParamContractError] if shares appear invalid
Contract C::ArrayOf[String] => C::Bool
def validate_all_shares(shares)
if shares_have_valid_headers!(shares) &&
Expand All @@ -299,9 +310,9 @@ def validate_all_shares(shares)

# Do all the shares have a valid first-byte index? They are invalid if not.
#
# @param shares_bytes [Array<Array>] the shares as Byte Arrays to be evaluated
# @return [true] returns true if there are enough shares
# @raise [TSS::ArgumentError] if shares appear invalid
# @param shares_bytes the shares as Byte Arrays to be evaluated
# @return returns true if there are enough shares
# @raise [ParamContractError, TSS::ArgumentError] if shares bytes appear invalid
Contract C::ArrayOf[C::ArrayOf[C::Num]] => C::Bool
def shares_bytes_have_valid_indexes!(shares_bytes)
u = shares_bytes.map do |s|
Expand All @@ -320,9 +331,9 @@ def shares_bytes_have_valid_indexes!(shares_bytes)
# Is it valid to use combinations mode? Only when there is an embedded non-zero
# hash_id Integer to test the results against. Invalid if not.
#
# @param hash_id [Integer] the shares as Byte Arrays to be evaluated
# @return [true] returns true if OK to use combinations mode
# @raise [TSS::ArgumentError] if hash_id represents a non hashing type
# @param hash_id the shares as Byte Arrays to be evaluated
# @return returns true if OK to use combinations mode
# @raise [ParamContractError, TSS::ArgumentError] if hash_id represents a non hashing type
Contract C::Int => C::Bool
def share_combinations_mode_allowed!(hash_id)
unless Hasher.codes_without_none.include?(hash_id)
Expand All @@ -340,11 +351,11 @@ def share_combinations_mode_allowed!(hash_id)
# e.g. 255 total shares, with threshold of 128, results in # combinations of:
# 2884329411724603169044874178931143443870105850987581016304218283632259375395
#
# @param shares [Array<String>] the shares to be evaluated
# @param threshold [Integer] the threshold value set in the shares
# @param max_combinations [Integer] the max (1_000_000) number of combinations allowed
# @return [true] returns true if a reasonable number of combinations
# @raise [TSS::ArgumentError] if the number of possible combinations is unreasonably high
# @param shares the shares to be evaluated
# @param threshold the threshold value set in the shares
# @param max_combinations the max (1_000_000) number of combinations allowed
# @return returns true if a reasonable number of combinations
# @raise [ParamContractError, TSS::ArgumentError] if args are invalid or the number of possible combinations is unreasonably high
Contract C::ArrayOf[String], C::Int, C::Int => C::Bool
def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000)
combinations = Util.calc_combinations(shares.size, threshold)
Expand Down
49 changes: 47 additions & 2 deletions lib/tss/custom_contracts.rb
@@ -1,10 +1,25 @@
module TSS
module Contracts

# Custom Contracts
# See : https://egonschiele.github.io/contracts.ruby/

class ArrayOfShares
def self.valid? val
val.present? &&
val.is_a?(Array) &&
val.length.between?(1,255) &&
Contracts::ArrayOf[String].valid?(val)
end

def self.to_s
'An Array of split secret shares'
end
end

class SecretArg
def self.valid? val
val.present? && val.is_a?(String) &&
val.present? &&
val.is_a?(String) &&
val.length.between?(1,65502) &&
['UTF-8', 'US-ASCII'].include?(val.encoding.name) &&
val.slice(0) != "\u001F"
Expand Down Expand Up @@ -52,6 +67,36 @@ def self.to_s
end
end

class HashAlgArg
def self.valid? val
Contracts::Enum['NONE', 'SHA1', 'SHA256'].valid?(val)
end

def self.to_s
'must be a uppercase String specifying the hash algorithm to use [NONE, SHA1, SHA256].'
end
end

class FormatArg
def self.valid? val
Contracts::Enum['BINARY', 'HUMAN'].valid?(val)
end

def self.to_s
'must be a uppercase String specifying the desired String share format [BINARY, HUMAN].'
end
end

class SelectByArg
def self.valid? val
Contracts::Enum['FIRST', 'SAMPLE', 'COMBINATIONS'].valid?(val)
end

def self.to_s
'must be a uppercase String specifying the desired way to sample shares provided [FIRST, SAMPLE, COMBINATIONS].'
end
end

class PadBlocksizeArg
def self.valid? val
val.present? &&
Expand Down

0 comments on commit c4fbcc5

Please sign in to comment.