Class: AliquotPay

Inherits:
Object
  • Object
show all
Defined in:
lib/aliquot-pay.rb,
lib/aliquot-pay/util.rb

Defined Under Namespace

Classes: Error, Util

Constant Summary collapse

EC_CURVE =
'prime256v1'.freeze
DEFAULTS =
{
  info: 'Google',
  recipient_id: 'merchant:0123456789',
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(protocol_version = :ECv2, root_key = nil) ⇒ AliquotPay

Returns a new instance of AliquotPay.



28
29
30
31
32
33
# File 'lib/aliquot-pay.rb', line 28

def initialize(protocol_version = :ECv2, root_key = nil)
  @protocol_version = protocol_version
  if root_key
    @root_key = root_key
  end
end

Instance Attribute Details

#auth_methodObject

Returns the value of attribute auth_method.



22
23
24
# File 'lib/aliquot-pay.rb', line 22

def auth_method
  @auth_method
end

#cleartext_messageObject

Returns the value of attribute cleartext_message.



20
21
22
# File 'lib/aliquot-pay.rb', line 20

def cleartext_message
  @cleartext_message
end

#cryptogramObject

Returns the value of attribute cryptogram.



23
24
25
# File 'lib/aliquot-pay.rb', line 23

def cryptogram
  @cryptogram
end

#eci_indicatorObject

Returns the value of attribute eci_indicator.



23
24
25
# File 'lib/aliquot-pay.rb', line 23

def eci_indicator
  @eci_indicator
end

#encrypted_messageObject

Returns the value of attribute encrypted_message.



20
21
22
# File 'lib/aliquot-pay.rb', line 20

def encrypted_message
  @encrypted_message
end

#ephemeral_public_keyObject

Returns the value of attribute ephemeral_public_key.



20
21
22
# File 'lib/aliquot-pay.rb', line 20

def ephemeral_public_key
  @ephemeral_public_key
end

#expiration_monthObject

Returns the value of attribute expiration_month.



22
23
24
# File 'lib/aliquot-pay.rb', line 22

def expiration_month
  @expiration_month
end

#expiration_yearObject

Returns the value of attribute expiration_year.



22
23
24
# File 'lib/aliquot-pay.rb', line 22

def expiration_year
  @expiration_year
end

#gateway_merchant_idObject

Returns the value of attribute gateway_merchant_id.



21
22
23
# File 'lib/aliquot-pay.rb', line 21

def gateway_merchant_id
  @gateway_merchant_id
end

#infoObject

Returns the value of attribute info.



25
26
27
# File 'lib/aliquot-pay.rb', line 25

def info
  @info
end

#intermediate_keyObject

Returns the value of attribute intermediate_key.



25
26
27
# File 'lib/aliquot-pay.rb', line 25

def intermediate_key
  @intermediate_key
end

#intermediate_signing_keyObject

Returns the value of attribute intermediate_signing_key.



17
18
19
# File 'lib/aliquot-pay.rb', line 17

def intermediate_signing_key
  @intermediate_signing_key
end

#key_expirationObject

Returns the value of attribute key_expiration.



19
20
21
# File 'lib/aliquot-pay.rb', line 19

def key_expiration
  @key_expiration
end

#key_valueObject

Returns the value of attribute key_value.



19
20
21
# File 'lib/aliquot-pay.rb', line 19

def key_value
  @key_value
end

#message_expirationObject

Returns the value of attribute message_expiration.



21
22
23
# File 'lib/aliquot-pay.rb', line 21

def message_expiration
  @message_expiration
end

#message_idObject

Returns the value of attribute message_id.



21
22
23
# File 'lib/aliquot-pay.rb', line 21

def message_id
  @message_id
end

#panObject

Returns the value of attribute pan.



22
23
24
# File 'lib/aliquot-pay.rb', line 22

def pan
  @pan
end

#payment_methodObject

Returns the value of attribute payment_method.



21
22
23
# File 'lib/aliquot-pay.rb', line 21

def payment_method
  @payment_method
end

#payment_method_detailsObject

Returns the value of attribute payment_method_details.



21
22
23
# File 'lib/aliquot-pay.rb', line 21

def payment_method_details
  @payment_method_details
end

#recipientObject

Returns the value of attribute recipient.



25
26
27
# File 'lib/aliquot-pay.rb', line 25

def recipient
  @recipient
end

#recipient_idObject



227
228
229
# File 'lib/aliquot-pay.rb', line 227

def recipient_id
  @recipient_id ||= DEFAULTS[:recipient_id]
end

#root_keyObject

Returns the value of attribute root_key.



25
26
27
# File 'lib/aliquot-pay.rb', line 25

def root_key
  @root_key
end

#shared_secretObject



231
232
233
234
235
# File 'lib/aliquot-pay.rb', line 231

def shared_secret
  return Base64.strict_encode64(@shared_secret) if @shared_secret
  @shared_secret ||= Random.new.bytes(32)
  shared_secret
end

#signatureObject

Returns the value of attribute signature.



17
18
19
# File 'lib/aliquot-pay.rb', line 17

def signature
  @signature
end

#signaturesObject

Returns the value of attribute signatures.



18
19
20
# File 'lib/aliquot-pay.rb', line 18

def signatures
  @signatures
end

#signed_keyObject

Returns the value of attribute signed_key.



18
19
20
# File 'lib/aliquot-pay.rb', line 18

def signed_key
  @signed_key
end

#signed_key_stringObject



163
164
165
# File 'lib/aliquot-pay.rb', line 163

def signed_key_string
  @signed_key_string ||= build_signed_key.to_json
end

#signed_messageObject

Returns the value of attribute signed_message.



17
18
19
# File 'lib/aliquot-pay.rb', line 17

def signed_message
  @signed_message
end

#tagObject

Returns the value of attribute tag.



20
21
22
# File 'lib/aliquot-pay.rb', line 20

def tag
  @tag
end

#tokenObject



35
36
37
# File 'lib/aliquot-pay.rb', line 35

def token
  build_token
end

Instance Method Details

#build_cleartext_messageObject



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/aliquot-pay.rb', line 109

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
  }

  if @protocol_version == :ECv2
    @cleartext_message.merge!(
      'gatewayMerchantId' => @gateway_merchant_id || 'SOME GATEWAY MERCHANT ID'
    )
  end

  @cleartext_message
end

#build_payment_method_detailsObject



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/aliquot-pay.rb', line 90

def build_payment_method_details
  return @payment_method_details if @payment_method_details
  value = {
    'pan'             => @pan              || '4111111111111111',
    'expirationYear'  => @expiration_year  || Time.now.year + 1,
    '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_signatureObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/aliquot-pay.rb', line 175

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_signaturesObject



194
195
196
197
198
199
200
201
202
203
# File 'lib/aliquot-pay.rb', line 194

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_keyObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/aliquot-pay.rb', line 146

def build_signed_key
  return @signed_key if @signed_key
  ensure_intermediate_key

  if !@intermediate_key.private_key? && !@intermediate_key.public_key?
    fail 'Intermediate key must be public and private key'
  end

  default_key_value      = Base64.strict_encode64(@intermediate_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_messageObject



131
132
133
134
135
136
137
138
139
140
# File 'lib/aliquot-pay.rb', line 131

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_tokenObject



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/aliquot-pay.rb', line 205

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

#encrypt(cleartext_message) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/aliquot-pay.rb', line 57

def encrypt(cleartext_message)
  @recipient ||= OpenSSL::PKey::EC.generate('prime256v1')
  @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_keyObject



171
172
173
# File 'lib/aliquot-pay.rb', line 171

def ensure_intermediate_key
  @intermediate_key ||= OpenSSL::PKey::EC.generate(EC_CURVE)
end

#ensure_root_keyObject



167
168
169
# File 'lib/aliquot-pay.rb', line 167

def ensure_root_key
  @root_key ||= OpenSSL::PKey::EC.generate(EC_CURVE)
end

#extract_root_signing_keysObject



39
40
41
42
43
44
45
46
47
# File 'lib/aliquot-pay.rb', line 39

def extract_root_signing_keys
  key = Base64.strict_encode64(ensure_root_key.to_der)
  {
    'keys' => [
      'protocolVersion' => @protocol_version,
      'keyValue'        => key,
    ]
  }.to_json
end

#sign(key, message) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/aliquot-pay.rb', line 49

def sign(key, message)
  d = OpenSSL::Digest::SHA256.new
  def key.private?
    private_key?
  end
  Base64.strict_encode64(key.sign(d, message))
end

#signed_message_stringObject



142
143
144
# File 'lib/aliquot-pay.rb', line 142

def signed_message_string
  @signed_message_string ||= build_signed_message.to_json
end