aliquot 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/aliquot/error.rb +16 -0
- data/lib/aliquot/validator.rb +184 -0
- data/lib/aliquot.rb +209 -0
- metadata +116 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 3a04779c8a107c40976456dfe5ef3192ab6e8b31ed215f542edd7ef8d0344e37
         | 
| 4 | 
            +
              data.tar.gz: 0c721e2aa5a54d9b4999dbd7e179aea62cf54bb93a077322e186299e5d079dae
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: d0eb73c56e32bda0d998b8206beec72a4ffef47127da0b149d5a052c7fdbe64a62e89cbb74e4502db29d6c3c05da1bba9197ccddb9b512410a65c0f39483bd68
         | 
| 7 | 
            +
              data.tar.gz: 4d778545daa1974f66f27502c5c9e851715b189bf7e0e2c06de3a2aedda258f5b8b831e77994e82cfeadb31e798a2507046248cc309853ee44a95ae09eb06b38
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            module Aliquot
         | 
| 2 | 
            +
              # Base class for all errors thrown in Aliquot
         | 
| 3 | 
            +
              class Error < StandardError; end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # Thrown if the token is expired
         | 
| 6 | 
            +
              class ExpiredException < Error; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              # Thrown if the signature is invalid
         | 
| 9 | 
            +
              class InvalidSignatureError < Error; end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              # Thrown if the MAC is invalid
         | 
| 12 | 
            +
              class InvalidMacError < Error; end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # Thrown if there was an error validating the input data
         | 
| 15 | 
            +
              class ValidationError < Error; end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,184 @@ | |
| 1 | 
            +
            require 'aliquot/error'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'base64'
         | 
| 4 | 
            +
            require 'dry-validation'
         | 
| 5 | 
            +
            require 'json'
         | 
| 6 | 
            +
            require 'openssl'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Aliquot
         | 
| 9 | 
            +
              module Validator
         | 
| 10 | 
            +
                # Verified according to:
         | 
| 11 | 
            +
                # https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography#payment-method-token-structure
         | 
| 12 | 
            +
                module Predicates
         | 
| 13 | 
            +
                  include Dry::Logic::Predicates
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  CUSTOM_PREDICATE_ERRORS = {
         | 
| 16 | 
            +
                    base64?:          'must be Base64',
         | 
| 17 | 
            +
                    pan?:             'must be a pan',
         | 
| 18 | 
            +
                    ec_public_key?:   'must be an EC public key',
         | 
| 19 | 
            +
                    eci?:             'must be an ECI',
         | 
| 20 | 
            +
                    json_string?:     'must be valid JSON',
         | 
| 21 | 
            +
                    integer_string?:  'must be string encoded integer',
         | 
| 22 | 
            +
                    month?:           'must be a month (1..12)',
         | 
| 23 | 
            +
                    year?:            'must be a year (2000..3000)',
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS requires eciIndicator',
         | 
| 26 | 
            +
                    authMethodCard:          'eciIndicator/cryptogram must be omitted when PAN_ONLY',
         | 
| 27 | 
            +
                  }.freeze
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Support Ruby 2.3, but use the faster #match? when available.
         | 
| 30 | 
            +
                  match_b = ''.respond_to?(:match?) ? ->(s, re) { s.match?(re) } : ->(s, re) { !!(s =~ re) }
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def self.to_bool(lbd)
         | 
| 33 | 
            +
                    lbd.call
         | 
| 34 | 
            +
                    true
         | 
| 35 | 
            +
                  rescue
         | 
| 36 | 
            +
                    false
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  predicate(:base64?) do |x|
         | 
| 40 | 
            +
                    str?(x) &&
         | 
| 41 | 
            +
                      match_b.call(x, /\A[=A-Za-z0-9+\/]*\z/) && # allowable chars
         | 
| 42 | 
            +
                      x.length.remainder(4).zero? && # multiple of 4
         | 
| 43 | 
            +
                      !match_b.call(x, /=[^$=]/) && # may only end with ='s
         | 
| 44 | 
            +
                      !match_b.call(x, /===/) # at most 2 ='s
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  # We should figure out how strict we should be. Hopefully we can discard
         | 
| 48 | 
            +
                  # the above Base64? predicate and use the following simpler one:
         | 
| 49 | 
            +
                  #predicate(:strict_base64?) { |x| !!Base64.strict_decode64(x) rescue false }
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  predicate(:pan?) { |x| match_b.call(x, /\A[1-9][0-9]{11,18}\z/) }
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  predicate(:eci?) { |x| str?(x) && match_b.call(x, /\A\d{1,2}\z/) }
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  predicate(:ec_public_key?) { |x| base64?(x) && OpenSSL::PKey::EC.new(Base64.decode64(x)).check_key rescue false }
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  predicate(:json_string?) { |x| !!JSON.parse(x) rescue false }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  predicate(:integer_string?) { |x| str?(x) && match_b.call(x, /\A\d+\z/) }
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  predicate(:month?) { |x| x.between?(1, 12) }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  predicate(:year?) { |x| x.between?(2000, 3000) }
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                # Base for DRY-Validation schemas used in Aliquot.
         | 
| 67 | 
            +
                class BaseSchema < Dry::Validation::Schema::JSON
         | 
| 68 | 
            +
                  predicates(Predicates)
         | 
| 69 | 
            +
                  def self.messages
         | 
| 70 | 
            +
                    super.merge(en: { errors: Predicates::CUSTOM_PREDICATE_ERRORS })
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                # DRY-Validation schema for Google Pay token
         | 
| 75 | 
            +
                TokenSchema = Dry::Validation.Schema(BaseSchema) do
         | 
| 76 | 
            +
                  required(:signature).filled(:str?, :base64?)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  # Currently supposed to be ECv1, but may evolve.
         | 
| 79 | 
            +
                  required(:protocolVersion).filled(:str?)
         | 
| 80 | 
            +
                  required(:signedMessage).filled(:str?, :json_string?)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                # DRY-Validation schema for signedMessage component Google Pay token
         | 
| 84 | 
            +
                SignedMessageSchema = Dry::Validation.Schema(BaseSchema) do
         | 
| 85 | 
            +
                  required(:encryptedMessage).filled(:str?, :base64?)
         | 
| 86 | 
            +
                  required(:ephemeralPublicKey).filled(:str?, :base64?)
         | 
| 87 | 
            +
                  required(:tag).filled(:str?, :base64?)
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                # DRY-Validation schema for paymentMethodDetails component Google Pay token
         | 
| 91 | 
            +
                PaymentMethodDetailsSchema = Dry::Validation.Schema(BaseSchema) do
         | 
| 92 | 
            +
                  required(:pan).filled(:integer_string?, :pan?)
         | 
| 93 | 
            +
                  required(:expirationMonth).filled(:int?, :month?)
         | 
| 94 | 
            +
                  required(:expirationYear).filled(:int?, :year?)
         | 
| 95 | 
            +
                  required(:authMethod).filled(:str?, included_in?: %w[PAN_ONLY CRYPTOGRAM_3DS])
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  optional(:cryptogram).filled(:str?)
         | 
| 98 | 
            +
                  optional(:eciIndicator).filled(:str?, :eci?)
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  rule('when authMethod is CRYPTOGRAM_3DS, cryptogram': %i[authMethod cryptogram]) do |method, cryptogram|
         | 
| 101 | 
            +
                    method.eql?('CRYPTOGRAM_3DS') > cryptogram.filled?
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  rule('when authMethod is PAN_ONLY, eciIndicator': %i[authMethod eciIndicator]) do |method, eci|
         | 
| 105 | 
            +
                    method.eql?('PAN_ONLY').then(eci.none?)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  rule('when authMethod is PAN_ONLY, cryptogram': %i[authMethod cryptogram]) do |method, cryptogram|
         | 
| 109 | 
            +
                    method.eql?('PAN_ONLY').then(cryptogram.none?)
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                # DRY-Validation schema for encryptedMessage component Google Pay token
         | 
| 114 | 
            +
                EncryptedMessageSchema = Dry::Validation.Schema(BaseSchema) do
         | 
| 115 | 
            +
                  required(:messageExpiration).filled(:str?, :integer_string?)
         | 
| 116 | 
            +
                  required(:messageId).filled(:str?)
         | 
| 117 | 
            +
                  required(:paymentMethod).filled(:str?, eql?: 'CARD')
         | 
| 118 | 
            +
                  required(:paymentMethodDetails).schema(PaymentMethodDetailsSchema)
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                module InstanceMethods
         | 
| 122 | 
            +
                  attr_reader :output
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  def validate
         | 
| 125 | 
            +
                    @validation ||= @schema.call(@input)
         | 
| 126 | 
            +
                    @output = @validation.output
         | 
| 127 | 
            +
                    return true if @validation.success?
         | 
| 128 | 
            +
                    raise Aliquot::ValidationError, "validation error: #{errors_formatted}"
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  def valid?
         | 
| 132 | 
            +
                    validate
         | 
| 133 | 
            +
                  rescue
         | 
| 134 | 
            +
                    false
         | 
| 135 | 
            +
                  end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  def errors
         | 
| 138 | 
            +
                    valid? unless @validation
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                    @validation.errors
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  def errors_formatted(node = [errors])
         | 
| 144 | 
            +
                    node.pop.flat_map do |key, value|
         | 
| 145 | 
            +
                      if value.is_a?(Array)
         | 
| 146 | 
            +
                        value.map { |error| "#{(node + [key]).join('.')} #{error}" }
         | 
| 147 | 
            +
                      else
         | 
| 148 | 
            +
                        errors_formatted(node + [key, value])
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                # Class for validating a Google Pay token
         | 
| 155 | 
            +
                class Token
         | 
| 156 | 
            +
                  include InstanceMethods
         | 
| 157 | 
            +
                  class Error < ::Aliquot::Error; end
         | 
| 158 | 
            +
                  def initialize(input)
         | 
| 159 | 
            +
                    @input = input
         | 
| 160 | 
            +
                    @schema = TokenSchema
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                # Class for validating the SignedMessage component of a Google Pay token
         | 
| 165 | 
            +
                class SignedMessage
         | 
| 166 | 
            +
                  include InstanceMethods
         | 
| 167 | 
            +
                  class Error < ::Aliquot::Error; end
         | 
| 168 | 
            +
                  def initialize(input)
         | 
| 169 | 
            +
                    @input = input
         | 
| 170 | 
            +
                    @schema = SignedMessageSchema
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                # Class for validating the encryptedMessage component of a Google Pay token
         | 
| 175 | 
            +
                class EncryptedMessageValidator
         | 
| 176 | 
            +
                  include InstanceMethods
         | 
| 177 | 
            +
                  class Error < ::Aliquot::Error; end
         | 
| 178 | 
            +
                  def initialize(input)
         | 
| 179 | 
            +
                    @input = input
         | 
| 180 | 
            +
                    @schema = EncryptedMessageSchema
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
              end
         | 
| 184 | 
            +
            end
         | 
    
        data/lib/aliquot.rb
    ADDED
    
    | @@ -0,0 +1,209 @@ | |
| 1 | 
            +
            require 'json'
         | 
| 2 | 
            +
            require 'base64'
         | 
| 3 | 
            +
            require 'excon'
         | 
| 4 | 
            +
            require 'hkdf'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'aliquot/validator'
         | 
| 7 | 
            +
            require 'aliquot/error'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            $key_updater_semaphore = Mutex.new
         | 
| 10 | 
            +
            $key_updater_thread = nil
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            module Aliquot
         | 
| 13 | 
            +
              ##
         | 
| 14 | 
            +
              # Constant-time comparison function
         | 
| 15 | 
            +
              def self.compare(a, b)
         | 
| 16 | 
            +
                err = 0
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                y = b.unpack('C*')
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                a.each_byte do |x|
         | 
| 21 | 
            +
                  err |= x ^ y.shift
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                err.zero?
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              ##
         | 
| 28 | 
            +
              # Keys used for signing in production
         | 
| 29 | 
            +
              SIGNING_KEY_URL = 'https://payments.developers.google.com/paymentmethodtoken/keys.json'.freeze
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              ##
         | 
| 32 | 
            +
              # Keys used for signing in a testing environment
         | 
| 33 | 
            +
              TEST_SIGNING_KEY_URL = 'https://payments.developers.google.com/paymentmethodtoken/test/keys.json'.freeze
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              ##
         | 
| 36 | 
            +
              # Start a thread that keeps the Google signing keys updated.
         | 
| 37 | 
            +
              def self.start_key_updater(logger)
         | 
| 38 | 
            +
                source = if ENV['ENVIRONMENT'] == 'production'
         | 
| 39 | 
            +
                           SIGNING_KEY_URL
         | 
| 40 | 
            +
                         else
         | 
| 41 | 
            +
                           TEST_SIGNING_KEY_URL
         | 
| 42 | 
            +
                         end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                $key_updater_semaphore.synchronize do
         | 
| 45 | 
            +
                  # Another thread might have been waiting for on the mutex
         | 
| 46 | 
            +
                  break unless $key_updater_thread.nil?
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  new_thread = Thread.new do
         | 
| 49 | 
            +
                    loop do
         | 
| 50 | 
            +
                      begin
         | 
| 51 | 
            +
                        timeout = 0
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                        conn = Excon.new(source)
         | 
| 54 | 
            +
                        resp = conn.get
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                        raise 'Unable to update keys: ' + resp.data[:status_line] unless resp.status == 200
         | 
| 57 | 
            +
                        cache_control = resp.headers['Cache-Control'].split(/,\s*/)
         | 
| 58 | 
            +
                        h = cache_control.map { |x| /\Amax-age=(?<timeout>\d+)\z/ =~ x; timeout }.compact
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                        timeout = h.first.to_i if h.length == 1
         | 
| 61 | 
            +
                        timeout = 86400 if timeout.nil? || !timeout.positive?
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                        Thread.current.thread_variable_set('keys', resp.body)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                        # Supposedly recommended by Tink library
         | 
| 66 | 
            +
                        sleep_time = timeout / 2
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                        logger.info('Updated Google signing keys. Sleeping for: ' + (sleep_time / 86400.0).to_s + ' days')
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                        sleep sleep_time
         | 
| 71 | 
            +
                      rescue Interrupt => e
         | 
| 72 | 
            +
                        # When interrupted
         | 
| 73 | 
            +
                        logger.fatal('Quitting: ' + e.message)
         | 
| 74 | 
            +
                        return
         | 
| 75 | 
            +
                      rescue => e
         | 
| 76 | 
            +
                        # Don't retry excessively.
         | 
| 77 | 
            +
                        logger.error('Exception updating Google signing keys: ' + e.message)
         | 
| 78 | 
            +
                        sleep 1
         | 
| 79 | 
            +
                      end
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  sleep 0.2 while new_thread.thread_variable_get('keys').nil?
         | 
| 84 | 
            +
                  # Body has now been set.
         | 
| 85 | 
            +
                  # Let other clients through.
         | 
| 86 | 
            +
                  $key_updater_thread = new_thread
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
              ##
         | 
| 91 | 
            +
              # A Payment represents a single payment using Google Pay.
         | 
| 92 | 
            +
              # It is used to verify/decrypt the supplied token by using the shared secret,
         | 
| 93 | 
            +
              # thus avoiding having knowledge of merchant primary keys.
         | 
| 94 | 
            +
              class Payment
         | 
| 95 | 
            +
                ##
         | 
| 96 | 
            +
                # Parameters:
         | 
| 97 | 
            +
                # token_string::  Google Pay token (JSON string)
         | 
| 98 | 
            +
                # shared_secret:: Base64 encoded shared secret
         | 
| 99 | 
            +
                # merchant_id::   Google Pay merchant ID ("merchant:<SOMETHING>")
         | 
| 100 | 
            +
                # logger::        The logger to use. Default: Logger.new($stdout)
         | 
| 101 | 
            +
                # signing_keys::  Formatted list of signing keys used to sign token contents.
         | 
| 102 | 
            +
                #                 Otherwise a thread continuously updating google signing
         | 
| 103 | 
            +
                #                 keys will be started.
         | 
| 104 | 
            +
                def initialize(token_string, shared_secret, merchant_id,
         | 
| 105 | 
            +
                               logger: Logger.new($stdout), signing_keys: nil)
         | 
| 106 | 
            +
                  Aliquot.start_key_updater(logger) if $key_updater_thread.nil? && signing_keys.nil?
         | 
| 107 | 
            +
                  @signing_keys = signing_keys
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  @shared_secret = shared_secret
         | 
| 110 | 
            +
                  @merchant_id = merchant_id
         | 
| 111 | 
            +
                  @token_string = token_string
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                ##
         | 
| 115 | 
            +
                # Validate and decrypt the token.
         | 
| 116 | 
            +
                def process
         | 
| 117 | 
            +
                  @token = JSON.parse(@token_string)
         | 
| 118 | 
            +
                  validate(Aliquot::Validator::Token, @token)
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  @protocol_version = @token['protocolVersion']
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  raise Error, 'only ECv1 protocolVersion is supported' unless @protocol_version == 'ECv1'
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  raise InvalidSignatureError unless valid_signature?(@token['signedMessage'],
         | 
| 125 | 
            +
                                                                      @token['signature'])
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  @signed_message = JSON.parse(@token['signedMessage'])
         | 
| 128 | 
            +
                  validate(Aliquot::Validator::SignedMessage, @signed_message)
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  aes_key, mac_key = derive_keys(@signed_message['ephemeralPublicKey'],
         | 
| 131 | 
            +
                                                 @shared_secret,
         | 
| 132 | 
            +
                                                 'Google')
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  raise InvalidMacError unless valid_mac?(mac_key,
         | 
| 135 | 
            +
                                                          @signed_message['encryptedMessage'],
         | 
| 136 | 
            +
                                                          @signed_message['tag'])
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  @message = decrypt(aes_key, @signed_message['encryptedMessage'])
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  validate(Aliquot::Validator::EncryptedMessageValidator, @message)
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  raise ExpiredException if expired?
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  @message
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                ##
         | 
| 148 | 
            +
                # Check if the token is expired, according to the messageExpiration included
         | 
| 149 | 
            +
                # in the token.
         | 
| 150 | 
            +
                def expired?
         | 
| 151 | 
            +
                  @message['messageExpiration'].to_f / 1000.0 <= Time.now.to_f
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                private
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                def validate(klass, data)
         | 
| 157 | 
            +
                  validator = klass.new(data)
         | 
| 158 | 
            +
                  validator.validate
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def derive_keys(ephemeral_public_key, shared_secret, info)
         | 
| 162 | 
            +
                  ikm = Base64.strict_decode64(ephemeral_public_key) +
         | 
| 163 | 
            +
                        Base64.strict_decode64(shared_secret)
         | 
| 164 | 
            +
                  hbytes = HKDF.new(ikm, algorithm: 'SHA256', info: info).next_bytes(32)
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  [hbytes[0..15], hbytes[16..32]]
         | 
| 167 | 
            +
                end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                def decrypt(key, encrypted)
         | 
| 170 | 
            +
                  c = OpenSSL::Cipher::AES128.new(:CTR)
         | 
| 171 | 
            +
                  c.key = key
         | 
| 172 | 
            +
                  c.decrypt
         | 
| 173 | 
            +
                  plain = c.update(Base64.strict_decode64(encrypted)) + c.final
         | 
| 174 | 
            +
                  JSON.parse(plain)
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                def valid_signature?(message, signature)
         | 
| 178 | 
            +
                  # Generate the string that was signed.
         | 
| 179 | 
            +
                  signed_string = ['Google', @merchant_id, @protocol_version, message].map do |str|
         | 
| 180 | 
            +
                    [str.length].pack('V') + str
         | 
| 181 | 
            +
                  end.join
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  keys = JSON.parse(signing_keys)['keys']
         | 
| 184 | 
            +
                  # Check if signature was performed with any possible key.
         | 
| 185 | 
            +
                  keys.map do |e|
         | 
| 186 | 
            +
                    next if e['protocolVersion'] != @protocol_version
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                    ec = OpenSSL::PKey::EC.new(Base64.strict_decode64(e['keyValue']))
         | 
| 189 | 
            +
                    d  = OpenSSL::Digest::SHA256.new
         | 
| 190 | 
            +
                    ec.verify(d, Base64.strict_decode64(signature), signed_string)
         | 
| 191 | 
            +
                  end.any?
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                def valid_mac?(mac_key, data, tag)
         | 
| 195 | 
            +
                  d = OpenSSL::Digest::SHA256.new
         | 
| 196 | 
            +
                  mac = OpenSSL::HMAC.digest(d, mac_key, Base64.strict_decode64(data))
         | 
| 197 | 
            +
                  mac = Base64.strict_encode64(mac)
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  return false if mac.length != tag.length
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                  Aliquot.compare(mac, tag)
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                def signing_keys
         | 
| 205 | 
            +
                  # Prefer static signing keys, otherwise fetch from updating thread.
         | 
| 206 | 
            +
                  @signing_keys || $key_updater_thread.thread_variable_get('keys')
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
              end
         | 
| 209 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,116 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: aliquot
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.9.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Clearhaus
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2018-10-03 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: dry-validation
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '0'
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: excon
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '0'
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '0'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: hkdf
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - "~>"
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - "~>"
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: aliquot-pay
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - "~>"
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - "~>"
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: rspec
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - "~>"
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '3'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - "~>"
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '3'
         | 
| 83 | 
            +
            description: 
         | 
| 84 | 
            +
            email: hello@clearhaus.com
         | 
| 85 | 
            +
            executables: []
         | 
| 86 | 
            +
            extensions: []
         | 
| 87 | 
            +
            extra_rdoc_files: []
         | 
| 88 | 
            +
            files:
         | 
| 89 | 
            +
            - lib/aliquot.rb
         | 
| 90 | 
            +
            - lib/aliquot/error.rb
         | 
| 91 | 
            +
            - lib/aliquot/validator.rb
         | 
| 92 | 
            +
            homepage: https://github.com/clearhaus/aliquot
         | 
| 93 | 
            +
            licenses:
         | 
| 94 | 
            +
            - MIT
         | 
| 95 | 
            +
            metadata: {}
         | 
| 96 | 
            +
            post_install_message: 
         | 
| 97 | 
            +
            rdoc_options: []
         | 
| 98 | 
            +
            require_paths:
         | 
| 99 | 
            +
            - lib
         | 
| 100 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 101 | 
            +
              requirements:
         | 
| 102 | 
            +
              - - ">="
         | 
| 103 | 
            +
                - !ruby/object:Gem::Version
         | 
| 104 | 
            +
                  version: '0'
         | 
| 105 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 106 | 
            +
              requirements:
         | 
| 107 | 
            +
              - - ">="
         | 
| 108 | 
            +
                - !ruby/object:Gem::Version
         | 
| 109 | 
            +
                  version: '0'
         | 
| 110 | 
            +
            requirements: []
         | 
| 111 | 
            +
            rubyforge_project: 
         | 
| 112 | 
            +
            rubygems_version: 2.7.7
         | 
| 113 | 
            +
            signing_key: 
         | 
| 114 | 
            +
            specification_version: 4
         | 
| 115 | 
            +
            summary: Validates Google Pay tokens
         | 
| 116 | 
            +
            test_files: []
         |