class TSS::Combiner

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.

Constants

C

Attributes

padding[R]
select_by[R]
shares[R]

Public Class Methods

new(opts = {}) click to toggle source
# File lib/tss/combiner.rb, line 18
def initialize(opts = {})
  # clone the incoming shares so the object passed to this
  # function doesn't get modified.
  @shares = opts.fetch(:shares).clone
  @select_by = opts.fetch(:select_by, 'FIRST')
  @padding = opts.fetch(:padding, true)
end

Public Instance Methods

combine() click to toggle source
# File lib/tss/combiner.rb, line 66
def combine
  # unwrap 'human' shares into binary shares
  if all_shares_appear_human?(shares)
    @shares = convert_shares_human_to_binary(shares)
  end

  validate_all_shares(shares)
  start_processing_time = Time.now

  h          = Util.extract_share_header(shares.sample)
  threshold  = h[:threshold]
  identifier = h[:identifier]
  hash_id    = h[:hash_id]

  # 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'
    @shares = shares.shift(threshold)
  elsif select_by == 'SAMPLE'
    @shares = shares.sample(threshold)
  end

  # slice out the data after the header bytes in each share
  # and unpack the byte string into an Array of Byte Arrays
  shares_bytes = shares.map do |s|
    bytestring = s.byteslice(Splitter::SHARE_HEADER_STRUCT.size..s.bytesize)
    bytestring.unpack('C*') unless bytestring.nil?
  end.compact

  shares_bytes_have_valid_indexes!(shares_bytes)

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

    # Build an Array of all possible `threshold` size combinations.
    share_combos = shares_bytes.combination(threshold).to_a

    # Try each combination until one works.
    secret = nil
    while secret.nil? && share_combos.present?
      # Check a combination and shift it off the Array
      result = extract_secret_from_shares!(hash_id, share_combos.shift)
      next if result.nil?
      secret = result
    end
  else
    secret = extract_secret_from_shares!(hash_id, shares_bytes)
  end

  # Return a Hash with the secret and metadata
  {
    hash: secret[:hash],
    hash_alg: secret[:hash_alg],
    identifier: identifier,
    process_time: ((Time.now - start_processing_time)*1000).round(2),
    secret: Util.bytes_to_utf8(secret[:secret]),
    threshold: threshold
  }
end

Private Instance Methods

all_shares_appear_human?(shares) click to toggle source
# File lib/tss/combiner.rb, line 192
def all_shares_appear_human?(shares)
  shares.all? do |s|
    # test for starting with 'tss' since regex match against
    # binary data sometimes throws exceptions.
    s.start_with?('tss~') && s.match(Util::HUMAN_SHARE_RE)
  end
end
convert_shares_human_to_binary(shares) click to toggle source
# File lib/tss/combiner.rb, line 206
def convert_shares_human_to_binary(shares)
  shares.map do |s|
    s_b64 = s.match(Util::HUMAN_SHARE_RE)
    if s_b64.present? && s_b64.to_a[1].present?
      begin
        # the [1] capture group contains the Base64 encoded bin share
        Base64.urlsafe_decode64(s_b64.to_a[1])
      rescue ArgumentError
        raise TSS::ArgumentError, 'invalid shares, some human format shares have invalid Base64 data'
      end
    else
      raise TSS::ArgumentError, 'invalid shares, some human format shares do not match expected pattern'
    end
  end
end
extract_secret_from_shares!(hash_id, shares_bytes) click to toggle source
# File lib/tss/combiner.rb, line 140
def extract_secret_from_shares!(hash_id, shares_bytes)
  secret = []

  # build up an Array of index values from each share
  # u[i] equal to the first octet of the ith share
  u = shares_bytes.map { |s| s[0] }

  # loop through each byte in all the shares
  # start at Array index 1 in each share's Byte Array to skip the index
  (1..(shares_bytes.first.length - 1)).each do |i|
    v = shares_bytes.map { |share| share[i] }
    secret << Util.lagrange_interpolation(u, v)
  end

  hash_alg = Hasher.key_from_code(hash_id)

  # Run the hash digest checks if the shares were created with a digest
  if Hasher.codes_without_none.include?(hash_id)
    # RTSS : pop off the hash digest bytes from the tail of the secret. This
    # leaves `secret` with only the secret bytes remaining.
    orig_hash_bytes = secret.pop(Hasher.bytesize(hash_alg))
    orig_hash_hex = Util.bytes_to_hex(orig_hash_bytes)

    # Remove PKCS#7 padding from the secret now that the hash
    # has been extracted from the data
    secret = Util.unpad(secret) if padding

    # RTSS : verify that the recombined secret computes the same hash
    # digest now as when it was originally created.
    new_hash_bytes = Hasher.byte_array(hash_alg, Util.bytes_to_utf8(secret))
    new_hash_hex = Util.bytes_to_hex(new_hash_bytes)

    unless Util.secure_compare(orig_hash_hex, new_hash_hex)
      raise TSS::InvalidSecretHashError, 'invalid shares, hash of secret does not equal embedded hash'
    end
  else
    secret = Util.unpad(secret) if padding
  end

  if secret.present?
    return { secret: secret, hash: orig_hash_hex, hash_alg: hash_alg }
  else
    raise TSS::NoSecretError, 'invalid shares, unable to recombine into a verifiable secret'
  end
end
share_combinations_mode_allowed!(hash_id) click to toggle source
# File lib/tss/combiner.rb, line 333
def share_combinations_mode_allowed!(hash_id)
  unless Hasher.codes_without_none.include?(hash_id)
    raise TSS::ArgumentError, 'invalid options, combinations mode can only be used with hashed shares.'
  else
    return true
  end
end
share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000) click to toggle source
# File lib/tss/combiner.rb, line 355
def share_combinations_out_of_bounds!(shares, threshold, max_combinations = 1_000_000)
  combinations = Util.calc_combinations(shares.size, threshold)
  if combinations > max_combinations
    raise TSS::ArgumentError, "invalid options, too many combinations (#{combinations})"
  else
    return true
  end
end
shares_bytes_have_valid_indexes!(shares_bytes) click to toggle source
# File lib/tss/combiner.rb, line 312
def shares_bytes_have_valid_indexes!(shares_bytes)
  u = shares_bytes.map do |s|
    raise TSS::ArgumentError, 'invalid shares, no index' if s[0].blank?
    raise TSS::ArgumentError, 'invalid shares, zero index' if s[0] == 0
    s[0]
  end

  unless u.uniq.size == shares_bytes.size
    raise TSS::ArgumentError, 'invalid shares, duplicate indexes'
  else
    return true
  end
end
shares_have_expected_length!(shares) click to toggle source
# File lib/tss/combiner.rb, line 265
def shares_have_expected_length!(shares)
  shares.each do |s|
    unless s.bytesize > Splitter::SHARE_HEADER_STRUCT.size + 1
      raise TSS::ArgumentError, 'invalid shares, too short'
    end
  end
  return true
end
shares_have_same_bytesize!(shares) click to toggle source
# File lib/tss/combiner.rb, line 228
def shares_have_same_bytesize!(shares)
  shares.each do |s|
    unless s.bytesize == shares.first.bytesize
      raise TSS::ArgumentError, 'invalid shares, different byte lengths'
    end
  end
  return true
end
shares_have_valid_headers!(shares) click to toggle source
# File lib/tss/combiner.rb, line 243
def shares_have_valid_headers!(shares)
  fh = Util.extract_share_header(shares.first)

  unless Contract.valid?(fh, ({ :identifier => String, :hash_id => C::Int, :threshold => C::Int, :share_len => C::Int }))
    raise TSS::ArgumentError, 'invalid shares, headers have invalid structure'
  end

  shares.each do |s|
    unless Util.extract_share_header(s) == fh
      raise TSS::ArgumentError, 'invalid shares, headers do not match'
    end
  end

  return true
end
shares_meet_threshold_min!(shares) click to toggle source
# File lib/tss/combiner.rb, line 280
def shares_meet_threshold_min!(shares)
  fh = Util.extract_share_header(shares.first)
  unless shares.size >= fh[:threshold]
    raise TSS::ArgumentError, 'invalid shares, fewer than threshold'
  else
    return true
  end
end
validate_all_shares(shares) click to toggle source
# File lib/tss/combiner.rb, line 295
def validate_all_shares(shares)
  if shares_have_valid_headers!(shares) &&
     shares_have_same_bytesize!(shares) &&
     shares_have_expected_length!(shares) &&
     shares_meet_threshold_min!(shares)
    return true
  else
    return false
  end
end