Class: Aliquot::Payment

Inherits:
Object
  • Object
show all
Defined in:
lib/aliquot/payment.rb

Overview

A Payment represents a single payment using Google Pay. It is used to verify/decrypt the supplied token by using the shared secret, thus avoiding having knowledge of any private keys involved.

Constant Summary collapse

SUPPORTED_PROTOCOL_VERSIONS =
%w[ECv1 ECv2].freeze

Instance Method Summary collapse

Constructor Details

#initialize(token_string, shared_secret, recipient_id, signing_keys: ENV['GOOGLE_SIGNING_KEYS']) ⇒ Payment

Parameters:

token_string

Google Pay token (JSON string)

shared_secret

Base64 encoded shared secret

recipient_id

Google Pay recipient ID

signing_keys

Signing keys fetched from Google



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/aliquot/payment.rb', line 22

def initialize(token_string, shared_secret, recipient_id,
               signing_keys: ENV['GOOGLE_SIGNING_KEYS'])

  begin
    validation = Aliquot::Validator::Token.new(JSON.parse(token_string))
    validation.validate
  rescue JSON::JSONError => e
    raise InputError, "token JSON is invalid, #{e.message}"
  end

  @token = validation.output

  @shared_secret = shared_secret
  @recipient_id   = recipient_id
  @signing_keys  = signing_keys
end

Instance Method Details

#check_shared_secretObject



111
112
113
114
115
116
117
118
119
# File 'lib/aliquot/payment.rb', line 111

def check_shared_secret
  begin
    decoded = Base64.strict_decode64(@shared_secret)
  rescue
    raise InvalidSharedSecretError, 'shared_secret must be base64'
  end

  raise InvalidSharedSecretError, 'shared_secret must be 32 bytes when base64 decoded' unless decoded.length == 32
end

#check_signatureObject



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/aliquot/payment.rb', line 121

def check_signature
  signed_string_message = ['Google', @recipient_id, protocol_version, @token[:signedMessage]].map do |str|
    [str.length].pack('V') + str
  end.join
  message_signature = Base64.strict_decode64(@token[:signature])

  root_signing_keys = root_keys

  case protocol_version
  when 'ECv1'
    # Check if signature was performed directly with any possible key.
    success =
      root_signing_keys.map do |key|
        key.verify(new_digest, message_signature, signed_string_message)
      end.any?

    raise InvalidSignatureError, 'signature of signedMessage does not match' unless success
  when 'ECv2'
    signed_key_signature = ['Google', 'ECv2', @token[:intermediateSigningKey][:signedKey]].map do |str|
      [str.length].pack('V') + str
    end.join

    # Check that the intermediate key signed the message
    pkey = OpenSSL::PKey::EC.new(Base64.strict_decode64(@intermediate_key[:keyValue]))
    raise InvalidSignatureError, 'signature of signedMessage does not match' unless pkey.verify(new_digest, message_signature, signed_string_message)

    intermediate_signatures = @token[:intermediateSigningKey][:signatures]

    # Check that a root signing key signed the intermediate
    success = valid_intermediate_key_signatures?(
      root_signing_keys,
      intermediate_signatures,
      signed_key_signature
    )

    raise InvalidSignatureError, 'no valid signature of intermediate key' unless success
  end
rescue OpenSSL::PKey::PKeyError => e
  # Catches problems with verifying signature. Can be caused by signature
  # being valid ASN1 but having invalid structure.
  raise InvalidSignatureError, "error verifying signature, #{e.message}"
end

#compare(a, b) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/aliquot/payment.rb', line 247

def compare(a, b)
  return false unless a.length == b.length

  diffs = 0

  ys = b.unpack('C*')

  a.each_byte do |x|
    diffs |= x ^ ys.shift
  end

  diffs.zero?
end

#decrypt(key, encrypted) ⇒ Object



211
212
213
214
215
216
217
# File 'lib/aliquot/payment.rb', line 211

def decrypt(key, encrypted)
  c = new_cipher
  c.decrypt
  c.key = key

  c.update(Base64.strict_decode64(encrypted)) + c.final
end

#derive_keys(ephemeral_public_key, shared_secret, info) ⇒ Object

Keys are derived according to the Google Pay specification.



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/aliquot/payment.rb', line 189

def derive_keys(ephemeral_public_key, shared_secret, info)
  input_keying_material = Base64.strict_decode64(ephemeral_public_key) + Base64.strict_decode64(shared_secret)

  key_len = new_cipher.key_len

  key_bytes = if OpenSSL.const_defined?(:KDF) && OpenSSL::KDF.respond_to?(:hkdf)
                OpenSSL::KDF.hkdf(input_keying_material, hash: new_digest, salt: '', length: 2 * key_len, info: info)
              else
                HKDF.new(input_keying_material, algorithm: 'SHA256', info: info).next_bytes(2 * key_len)
              end

  [key_bytes[0..key_len - 1], key_bytes[key_len..2 * key_len]]
end

#expired?Boolean

Check if the token is expired, according to the messageExpiration included in the token.

Returns:

  • (Boolean)


230
231
232
# File 'lib/aliquot/payment.rb', line 230

def expired?
  @message[:messageExpiration].to_f / 1000.0 <= Time.now.to_f
end

#intermediate_key_expired?Boolean

Returns:

  • (Boolean)


100
101
102
103
# File 'lib/aliquot/payment.rb', line 100

def intermediate_key_expired?
  cur_millis = (Time.now.to_f * 1000).round
  @intermediate_key[:keyExpiration].to_i < cur_millis
end

#new_cipherObject



234
235
236
237
238
239
240
241
# File 'lib/aliquot/payment.rb', line 234

def new_cipher
  case protocol_version
  when 'ECv1'
    OpenSSL::Cipher::AES128.new(:CTR)
  when 'ECv2'
    OpenSSL::Cipher::AES256.new(:CTR)
  end
end

#new_digestObject



243
244
245
# File 'lib/aliquot/payment.rb', line 243

def new_digest
  OpenSSL::Digest::SHA256.new
end

#processObject

Validate and decrypt the token.

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/aliquot/payment.rb', line 41

def process
  unless valid_protocol_version?
    raise Error, "supported protocol versions are #{SUPPORTED_PROTOCOL_VERSIONS.join(', ')}"
  end

  @recipient_id = validate_recipient_id

  check_shared_secret

  if protocol_version == 'ECv2'
    @intermediate_key = validate_intermediate_key
    raise InvalidSignatureError, 'intermediate certificate is expired' if intermediate_key_expired?
  end

  check_signature

  @signed_message = validate_signed_message

  begin
    aes_key, mac_key = derive_keys(@signed_message[:ephemeralPublicKey], @shared_secret, 'Google')
  rescue => e
    raise KeyDerivationError, "cannot derive keys, #{e.message}"
  end

  raise InvalidMacError, 'MAC does not match' unless valid_mac?(mac_key)

  begin
    @message = JSON.parse(decrypt(aes_key, @signed_message[:encryptedMessage]))
  rescue JSON::JSONError => e
    raise InputError, "encryptedMessage JSON is invalid, #{e.message}"
  rescue => e
    raise DecryptionError, "decryption failed, #{e.message}"
  end

  @message = validate_message

  raise TokenExpiredError, 'token is expired' if expired?

  @message
end

#protocol_versionObject



82
83
84
# File 'lib/aliquot/payment.rb', line 82

def protocol_version
  @token[:protocolVersion]
end

#root_keysObject



164
165
166
167
168
169
170
171
172
# File 'lib/aliquot/payment.rb', line 164

def root_keys
  root_signing_keys = JSON.parse(@signing_keys)['keys'].select do |key|
    key['protocolVersion'] == protocol_version
  end

  root_signing_keys.map! do |key|
    OpenSSL::PKey::EC.new(Base64.strict_decode64(key['keyValue']))
  end
end

#valid_intermediate_key_signatures?(signing_keys, signatures, signed) ⇒ Boolean

Returns:

  • (Boolean)


174
175
176
177
178
179
# File 'lib/aliquot/payment.rb', line 174

def valid_intermediate_key_signatures?(signing_keys, signatures, signed)
  signing_keys.product(signatures).each do |key, sig|
    return true if key.verify(new_digest, Base64.strict_decode64(sig), signed)
  end
  false
end

#valid_mac?(mac_key) ⇒ Boolean

Returns:

  • (Boolean)


203
204
205
206
207
208
209
# File 'lib/aliquot/payment.rb', line 203

def valid_mac?(mac_key)
  data = Base64.strict_decode64(@signed_message[:encryptedMessage])
  tag = @signed_message[:tag]
  mac = OpenSSL::HMAC.digest(new_digest, mac_key, data)

  compare(Base64.strict_encode64(mac), tag)
end

#valid_protocol_version?Boolean

Returns:

  • (Boolean)


86
87
88
# File 'lib/aliquot/payment.rb', line 86

def valid_protocol_version?
  SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
end

#validate_intermediate_keyObject



90
91
92
93
94
95
96
97
98
# File 'lib/aliquot/payment.rb', line 90

def validate_intermediate_key
  # Valid JSON as it has been checked by Token Validator.
  intermediate_key = JSON.parse(@token[:intermediateSigningKey][:signedKey])

  validator = Aliquot::Validator::SignedKeyValidator.new(intermediate_key)
  validator.validate

  validator.output
end

#validate_messageObject



219
220
221
222
223
224
225
# File 'lib/aliquot/payment.rb', line 219

def validate_message
  validator = Aliquot::Validator::EncryptedMessageValidator.new(@message)
  validator.validate

  # Output is hashed with symbolized keys.
  validator.output
end

#validate_recipient_idObject



105
106
107
108
109
# File 'lib/aliquot/payment.rb', line 105

def validate_recipient_id
  raise InvalidRecipientIDError, 'recipient_id must be alphanumeric and punctuation' unless /\A[[:graph:]]+\z/ =~ @recipient_id

  @recipient_id
end

#validate_signed_messageObject



181
182
183
184
185
186
# File 'lib/aliquot/payment.rb', line 181

def validate_signed_message
  signed_message = @token[:signedMessage]
  validator = Aliquot::Validator::SignedMessage.new(JSON.parse(signed_message))
  validator.validate
  validator.output
end