class AliquotPay

Constants

DEFAULTS
EC_CURVE

Attributes

auth_method[RW]
cleartext_message[RW]
cryptogram[RW]
eci_indicator[RW]
encrypted_message[RW]
ephemeral_public_key[RW]
expiration_month[RW]
expiration_year[RW]
info[RW]
intermediate_key[RW]
intermediate_signing_key[RW]
key_expiration[RW]
key_value[RW]
message_expiration[RW]
message_id[RW]
pan[RW]
payment_method[RW]
payment_method_details[RW]
recipient[RW]
recipient_id[W]
root_key[RW]
shared_secret[W]
signature[RW]
signatures[RW]
signed_key[RW]
signed_key_string[W]
signed_message[RW]
tag[RW]
token[W]

Public Class Methods

new(protocol_version = :ECv2) click to toggle source
# File lib/aliquot-pay.rb, line 28
def initialize(protocol_version = :ECv2)
  @protocol_version = protocol_version
end

Public Instance Methods

build_cleartext_message() click to toggle source
# File lib/aliquot-pay.rb, line 114
def build_cleartext_message
  return @cleartext_message if @cleartext_message
  default_message_id = Base64.strict_encode64(OpenSSL::Random.random_bytes(24))
  default_message_expiration = ((Time.now.to_f + 60 * 5) * 1000).round.to_s

  @cleartext_message = {
    'messageExpiration'    => @message_expiration || default_message_expiration,
    'messageId'            => @message_id || default_message_id,
    'paymentMethod'        => @payment_method || 'CARD',
    'paymentMethodDetails' => build_payment_method_details
  }
end
build_payment_method_details() click to toggle source
# File lib/aliquot-pay.rb, line 95
def build_payment_method_details
  return @payment_method_details if @payment_method_details
  value = {
    'pan'             => @pan              || '4111111111111111',
    'expirationYear'  => @expiration_year  || 2023,
    'expirationMonth' => @expiration_month || 12,
    'authMethod'      => @auth_method      || 'PAN_ONLY',
  }

  if @auth_method == 'CRYPTOGRAM_3DS'
    value.merge!(
      'cryptogram'   => @cryptogram    || 'SOME CRYPTOGRAM',
      'eciIndicator' => @eci_indicator || '05'
    )
  end

  value
end
build_signature() click to toggle source
# File lib/aliquot-pay.rb, line 173
def build_signature
  return @signature if @signature
  key = case @protocol_version
        when :ECv1
          ensure_root_key
        when :ECv2
          ensure_intermediate_key
        end

  signature_string =
    signed_string_message = ['Google',
                             recipient_id,
                             @protocol_version.to_s,
                             signed_message_string].map do |str|
      [str.length].pack('V') + str
    end.join
  @signature = sign(key, signature_string)
end
build_signatures() click to toggle source
# File lib/aliquot-pay.rb, line 192
def build_signatures
  return @signatures if @signatures

  signature_string =
    signed_key_signature = ['Google', 'ECv2', signed_key_string].map do |str|
      [str.to_s.length].pack('V') + str.to_s
    end.join

  @signatures = [sign(ensure_root_key, signature_string)]
end
build_signed_key() click to toggle source
# File lib/aliquot-pay.rb, line 142
def build_signed_key
  return @signed_key if @signed_key
  ensure_intermediate_key

  if @intermediate_key.private_key? || @intermediate_key.public_key?
    public_key = eckey_to_public(@intermediate_key)
  else
    fail 'Intermediate key must be public and private key'
  end

  default_key_value      = Base64.strict_encode64(public_key.to_der)
  default_key_expiration = "#{Time.now.to_i + 3600}000"

  @signed_key = {
    'keyExpiration' => @key_expiration || default_key_expiration,
    'keyValue'      => @key_value || default_key_value,
  }
end
build_signed_message() click to toggle source
# File lib/aliquot-pay.rb, line 127
def build_signed_message
  return @signed_message if @signed_message

  signed_message = encrypt(build_cleartext_message.to_json)
  signed_message['encryptedMessage']   = @encrypted_message if @encrypted_message
  signed_message['ephemeralPublicKey'] = @ephemeral_public_key if @ephemeral_public_key
  signed_message['tag']                = @tag if @tag

  @signed_message = signed_message
end
build_token() click to toggle source
# File lib/aliquot-pay.rb, line 203
def build_token
  return @token if @token
  res = {
    'protocolVersion' => @protocol_version.to_s,
    'signedMessage'   => @signed_message || signed_message_string,
    'signature'       => build_signature,
  }

  if @protocol_version == :ECv2
    intermediate = {
      'intermediateSigningKey' => @intermediate_signing_key || {
        'signedKey'  => signed_key_string,
        'signatures' => build_signatures,
      }
    }

    res.merge!(intermediate)
  end

  @token = res
end
eckey_to_public(key) click to toggle source
# File lib/aliquot-pay.rb, line 46
def eckey_to_public(key)
  p = OpenSSL::PKey::EC.new(EC_CURVE)

  p.public_key = key.public_key

  p
end
encrypt(cleartext_message) click to toggle source
# File lib/aliquot-pay.rb, line 62
def encrypt(cleartext_message)
  @recipient ||= OpenSSL::PKey::EC.new('prime256v1').generate_key
  @info ||= 'Google'

  eph = AliquotPay::Util.generate_ephemeral_key
  @shared_secret ||= AliquotPay::Util.generate_shared_secret(eph, @recipient.public_key)
  ss  = @shared_secret

  case @protocol_version
  when :ECv1
    cipher = OpenSSL::Cipher::AES128.new(:CTR)
  when :ECv2
    cipher = OpenSSL::Cipher::AES256.new(:CTR)
  else
    raise StandardError, "Invalid protocol_version #{protocol_version}"
  end

  keys = AliquotPay::Util.derive_keys(eph.public_key.to_bn.to_s(2), ss, @info, @protocol_version)

  cipher.encrypt
  cipher.key = keys[:aes_key]

  encrypted_message = cipher.update(cleartext_message) + cipher.final

  tag = AliquotPay::Util.calculate_tag(keys[:mac_key], encrypted_message)

  {
    'encryptedMessage'   => Base64.strict_encode64(encrypted_message),
    'ephemeralPublicKey' => Base64.strict_encode64(eph.public_key.to_bn.to_s(2)),
    'tag'                => Base64.strict_encode64(tag),
  }
end
ensure_intermediate_key() click to toggle source
# File lib/aliquot-pay.rb, line 169
def ensure_intermediate_key
  @intermediate_key ||= OpenSSL::PKey::EC.new(EC_CURVE).generate_key
end
ensure_root_key() click to toggle source
# File lib/aliquot-pay.rb, line 165
def ensure_root_key
  @root_key ||= OpenSSL::PKey::EC.new(EC_CURVE).generate_key
end
extract_root_signing_keys() click to toggle source
# File lib/aliquot-pay.rb, line 36
def extract_root_signing_keys
  key = Base64.strict_encode64(eckey_to_public(ensure_root_key).to_der)
  {
    'keys' => [
      'protocolVersion' => @protocol_version,
      'keyValue'        => key,
    ]
  }.to_json
end
recipient_id() click to toggle source
# File lib/aliquot-pay.rb, line 225
def recipient_id
  @recipient_id ||= DEFAULTS[:recipient_id]
end
shared_secret() click to toggle source
# File lib/aliquot-pay.rb, line 229
def shared_secret
  return Base64.strict_encode64(@shared_secret) if @shared_secret
  @shared_secret ||= Random.new.bytes(32)
  shared_secret
end
sign(key, message) click to toggle source

private

# File lib/aliquot-pay.rb, line 56
def sign(key, message)
  d = OpenSSL::Digest::SHA256.new
  def key.private?; private_key?; end
  Base64.strict_encode64(key.sign(d, message))
end
signed_key_string() click to toggle source
# File lib/aliquot-pay.rb, line 161
def signed_key_string
  @signed_key_string ||= build_signed_key.to_json
end
signed_message_string() click to toggle source
# File lib/aliquot-pay.rb, line 138
def signed_message_string
  @signed_message_string ||= build_signed_message.to_json
end
token() click to toggle source
# File lib/aliquot-pay.rb, line 32
def token
  build_token
end