grempe/secretsharing

View on GitHub
lib/secretsharing/shamir/secret.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# -*- encoding: utf-8 -*-

# Copyright 2011-2015 Glenn Rempe

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module SecretSharing
  module Shamir
    # A SecretSharing::Shamir::Secret object represents a Secret in the
    # Shamir secret sharing scheme. Secrets can be passed in as an input
    # argument when creating a new SecretSharing::Shamir::Container or
    # can be the output from a Container that has successfully decoded shares.
    # A new Secret take 0 or 1 args. Zero args means the Secret will be initialized
    # with a random Numeric object with the Secret::DEFAULT_BITLENGTH. If a
    # single argument is passed it can be a String, or Integer.
    # If its a String, its expected to be of a special encoding
    # that was generated as the output of calling #to_s on another Secret object.
    # If the object type is an Integer it can be up to 4096 bits in length.
    #
    # All secrets are internally represented as a Numeric which can be retrieved
    # in its raw form using #secret.
    #
    class Secret
      include SecretSharing::Shamir

      # FIXME : Is a MAX_BITLENGTH really needed?  Can it be larger if so?

      MAX_BITLENGTH = 4096

      attr_accessor :bitlength, :hmac
      attr_reader :secret

      # FIXME : allow instantiating a secret with any random number bitlength you choose.

      # rubocop:disable Metrics/PerceivedComplexity
      def initialize(opts = {})
        opts = {
          :secret => get_random_number(32) # 32 Bytes, 256 Bits
        }.merge!(opts)

        # override with options
        opts.each_key do |k|
          if self.respond_to?("#{k}=")
            send("#{k}=", opts[k])
          else
            fail ArgumentError, "Argument '#{k}' is not allowed"
          end
        end

        # FIXME : Do we really need the ability for a String arg to re-instantiate a Secret?
        # FIXME : If its a String, shouldn't it be able to be an arbitrary String converted to/from a Number?

        if opts[:secret].is_a?(String)
          # Decode a Base64.urlsafe_encode64 String which contains a Base 36 encoded Bignum back into a Bignum
          # See : Secret#to_s for forward encoding method.
          stripped_secret = opts[:secret].strip
          fail ArgumentError, 'invalid secret (empty String)' if stripped_secret.empty?
          decoded_secret = Base64.urlsafe_decode64(stripped_secret)
          fail ArgumentError, 'invalid secret (base64 decode returned nil or empty String)' if decoded_secret.empty?
          int_secret = decoded_secret.to_i(36)
          fail ArgumentError, 'invalid secret (not an Integer)' unless int_secret.is_a?(Integer)
          fail ArgumentError, 'invalid secret (Integer bit length < 100)' unless int_secret.bit_length > 100
          @secret = int_secret
        end

        @secret = opts[:secret] if @secret.nil?
        fail ArgumentError, "Secret must be an Integer, not a '#{@secret.class}'" unless @secret.is_a?(Integer)

        # Get the number of binary bits in this secret's value.
        @bitlength = @secret.bit_length

        fail ArgumentError, "Secret must have a bitlength less than or equal to #{MAX_BITLENGTH}" if @bitlength > MAX_BITLENGTH

        generate_hmac
      end
      # rubocop:enable Metrics/PerceivedComplexity

      # Secrets are equal if the Numeric in @secret is the same.
      # Do secure constant-time comparison of the objects.
      def ==(other)
        other_secret_hash = RbNaCl::Hash.blake2b(other.secret.to_s, digest_size: 32)
        own_secret_hash   = RbNaCl::Hash.blake2b(@secret.to_s, digest_size: 32)
        RbNaCl::Util.verify32(other_secret_hash, own_secret_hash)
      end

      # Set a new secret forces regeneration of the HMAC
      def secret=(secret)
        @secret = secret
        generate_hmac
      end

      def secret?
        @secret.is_a?(Integer)
      end

      def to_s
        # Convert the Bignum to a Base 36 encoded String
        # Wrap the Base 36 encoded String as a URL safe Base 64 encoded String
        # Combined this should result in a relatively compact and portable String
        Base64.urlsafe_encode64(@secret.to_s(36))
      end

      # See : generate_hmac
      def valid_hmac?
        return false if !@secret.is_a?(Integer) || @hmac.to_s.empty? || @secret.to_s.empty?
        hash = RbNaCl::Hash.sha512(@secret.to_s)
        key = hash[0, 32]
        authenticator = RbNaCl::Util.hex2bin(@hmac)
        msg = hash[33, 64]
        begin
          RbNaCl::HMAC::SHA256.verify(key, authenticator, msg)
        rescue
          false
        end
      end

      private

      # SHA512 over @secret returns a 64 Byte array. Use the first 32 bytes
      # as the HMAC key, and the last 32 bytes as the message.
      #
      # This will allow a point of comparison between the original secret that
      # was split into shares, and the secret that was retrieved by combining shares.
      def generate_hmac
        return false if @secret.to_s.empty?
        hash = RbNaCl::Hash.sha512(@secret.to_s)
        key = hash[0, 32]
        msg = hash[33, 64]
        @hmac = RbNaCl::Util.bin2hex(RbNaCl::HMAC::SHA256.auth(key, msg))
      end
    end # class Secret
  end # module Shamir
end # module SecretSharing