Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Pay support #10

Closed

Conversation

bdewater
Copy link
Contributor

@bdewater bdewater commented Nov 7, 2017

This PR adds support for Google Pay tokens as returned by the Google Payment API. Google's announcement is here and the technical docs are here.

A tl;dr version is that what used to be known as 'Android Pay on the web' is now known as TOKENIZED_CARD, while CARD is the credit card stored on a Google account. In the browser both go through W3C Payment Request with the https://google.com/pay payment method.

All this is versioned as ECv1 in the technical docs, old Android Pay tokens are retconned as ECv0. For decryption, an important difference between ECv1 TOKENIZED_CARD and ECv0 is the 'info' parameter in the key derivation step. Other than that the algorithms are the same.

ECv1 data is signed by Google. A Google provided Merchant ID is needed to verify it. The URLs to fetch the keys can be found in the Tink reference implementation. An application should store the keys somewhere and respect the expires header to periodically refresh them. For now I've left this as an application concern. The code in this PR takes the parsed JSON and is able to handle multiple keys when Google rotates the keys.

message['paymentMethodDetails']
end

class << self
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

best to move classes/class methods to the top of the file to avoid them being in a private section (which will have no effect other than confuse people)

hkdf_keys = {
def derive_hkdf_keys(ephemeral_public_key, shared_secret, info = 'Android')
key_material = Base64.decode64(ephemeral_public_key) + shared_secret
hkdf = HKDF.new(key_material, :algorithm => 'SHA256', :info => info)
Copy link

@elfassy elfassy Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IRL: may want to have the info be a constant since only a few keys are allowed. Might also be worth having another class for android

class AndroidPaymentToken < PaymentToken
  HKDF_INFO = 'Android'
end
hkdf = HKDF.new(key_material, :algorithm => 'SHA256', :info => self.class.const_get('HKDF_INFO')

@bdewater bdewater force-pushed the scrappy-google-payment-tokens branch from 14d9d0c to 418e2b6 Compare December 18, 2017 04:12
@bdewater bdewater changed the title Pay with Google support Google Pay support Jan 9, 2018
@rwdaigle
Copy link
Contributor

Hi @bdewater - this is great, thank you!

I'm wondering if we can isolate the Google Pay specific parts of this PR from some of the other changes you've made (upgrading the Ruby version, test framework changes, etc...)? I think we're ok with the majority of them, but just want to be able to evaluate them independently.

@bdewater
Copy link
Contributor Author

bdewater commented Jan 23, 2018

Hey @rwdaigle thanks for the kind words. That should definitely be possible!

I'm thinking it would be best to pull those 'plumbing' commits out in a separate PR, get that merged to master, and then rebase this on top of it. What do you think?

@rwdaigle
Copy link
Contributor

I'm thinking it would be best to pull those 'plumbing' commits out in a separate PR, get that merged to master, and then rebase this on top of it. What do you think?

Sure, let's get the easy stuff out of the way first 👍

Re: the ruby version change - I'd love for that not to be a thing baked into the lib itself. Perhaps we should just gitignore .ruby-version?

@bdewater bdewater mentioned this pull request Feb 2, 2018
…yment tokens

Docs are at https://developers.google.com/payments/payment-data-cryptography

This commit is a quick and dirty attempt to have Google Payment API TOKENIZED_CARD working by returning the same values
as Android Pay did, without breaking the existing Android Pay implementation that can still be used by non-browser
clients.

This is done by:
- using either 'Android' or 'Google' for the info parameter of the KDF
- return the parsed paymentMethodDetails attribute from the encryptedMessage of a Google Payment token, containing 'dpan' etc,
  to the consuming application

Stuff missing in this commit for a complete implementation:
- Verifying the signature (from Google) of the signedMessage
- Checking the messageExpiration timestamp in the encryptedMessage
With signature verification and message expiration check, this limitation from the inital scrappy implementation can be removed.

This also means the consuming application has full access to details like paymentMethod, which can be used for example with ActiveMerchant to build either a ActiveMerchant::Billing::NetworkTokenizationCreditCard or a ActiveMerchant::Billing::CreditCard object.
@bdewater bdewater force-pushed the scrappy-google-payment-tokens branch from 4b2a1cc to cc60cd6 Compare February 14, 2018 09:34
@bdewater
Copy link
Contributor Author

@rwdaigle I just pushed the rebase :)

@bdewater
Copy link
Contributor Author

@rwdaigle we're live https://www.shopify.ca/google-pay so I think I can call this production ready now ;)

@rwdaigle
Copy link
Contributor

@bdewater awesome (and congrats!).

I'll try to raise this a bit on my stack.

@rwdaigle
Copy link
Contributor

@bdewater question - is Android Pay deprecated? I.e. is there any reason to support both token formats in the same lib version (would an app/service need to be able to handle both at the same time), or perhaps we can tag r2d2 as-is for Android Pay support and release a new version w/ only Google Pay support going forward?

@bdewater
Copy link
Contributor Author

@rwdaigle when I started implementing this Google disabled Android Pay signups but we still saw the occasional purchase coming in, so we decided to implement Google Pay in parallel to not break anything before we actually had the alternative in place.

I'm not sure what the current state of all this is, but I can ask around. Starting from scratch would definitely clean some things up.

@rwdaigle
Copy link
Contributor

Starting from scratch would definitely clean some things up.

It may not be viable (I'm checking on our end too), but let me know what you find. Thanks! 👍

@rwdaigle
Copy link
Contributor

rwdaigle commented May 3, 2018

FYI @deedeelavinder and @dtykocki are taking a look at this now...

Copy link
Contributor

@rwdaigle rwdaigle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deedeelavinder just took a pass through this PR and, outside of the specific comments, have the following thought:

I'm not sure the new GooglePaymentToken class needs to be a subclass of the previous (and current Android) PaymentToken class. I looked but didn't see much shared code between the two and, conceptually, don't believe Google Pay should be thought of as a special case of Android Pay? It feels, from my high level, that GooglePaymentToken should be its own class, and a new AndroidPaymentToken should be what PaymentToken is now. If there is functionality to share it can be done via modules or a shared utility library vs. inheritance. (@bdewater may have some insight to share here that disputes this take given his deeper understanding of the implementation).

end.join
end

def initialize(token_attrs, recipient_id:, verification_keys:)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is Ruby v2.? syntax? If this is the only place we're using this it might be good to revert to "legacy" syntax to support a greater range of Ruby versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mandatory keyword args were introduced in Ruby 2.2, which was EOL'ed end of March after being released over 3 years ago. Given that we're dealing with credit card data, should we make the interface less clear to accommodate for old Ruby versions? Personally, I don't think we should.

@@ -27,7 +30,8 @@ def decrypt(private_key_pem)
# verify the tag is a valid value
self.class.verify_mac(digest, hkdf_keys[:mac_key], encrypted_message, tag)

self.class.decrypt_message(encrypted_message, hkdf_keys[:symmetric_encryption_key])
message = self.class.decrypt_message(encrypted_message, hkdf_keys[:symmetric_encryption_key])
JSON.parse(message)
Copy link
Contributor

@rwdaigle rwdaigle May 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current contract of r2d2 is that it tries to assume as little as possible about how the caller wants to parse the resulting message. This has the added benefit of letting the caller use their own JSON parser, or defer to a later point in processing to do so. I've found that people are quite opinionated about their chosen JSON lib :) I'd like to retain that contract here, and just return the message string itself, letting the caller decide what they do with it from there. (further background here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GooglePaymentToken needs the parsed JSON for the message expiration check. It's logical to have both decrypt methods should be returning the same type.

The Ruby community seems to have mostly abandoned multi-json and such in libraries in favor of stdlib json; it's available on all modern Rubies for years and good enough. Call your equivalent of Oj.mimic_JSON if you really feel the need for speed, but the last time I looked at performance for this gem the Ruby HKDF was the slowest part IIRC (see ruby/openssl#172).

@@ -0,0 +1,5 @@
{
"signature": "MEUCIQDzgQkY/Lj42FoZC9wN44Uz0r6uDttc68k2c9oZCzNJPwIgSRukEx30uMjqzFswekoarAtO9p1/E221hNmVuHzDW+M\u003d",
"protocolVersion": "ECv1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This payment token structure looks very different from the one we have for Android Pay. If this is a structure we can rely on, it would be interesting to provide a single entry point into r2d2 that determines which token path to use to decrypt vs. having the user have to peek into the structure to determine which token type it is.

Not sure if this is a real world scenario or not? Definitely would still keep very explicit Google vs. Android token types, just with a thin routing convenience on top if we wanted to provide this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android Pay was unversioned and Google's documentation says in this case to assume ECv0. A factory method would definitely be nice to abstract this concern. I can add this somewhere this week.

@bdewater
Copy link
Contributor Author

Thanks for your time to review @rwdaigle and @deedeelavinder!

I'm not sure the new GooglePaymentToken class needs to be a subclass of the previous (and current Android) PaymentToken class. I looked but didn't see much shared code between the two and, conceptually, don't believe Google Pay should be thought of as a special case of Android Pay?

The innermost payload of a Google Pay TOKENIZED_CARD token is essentially the same as Android Pay (docs). The PR commits are very much an exploration of how everything fitted together at the time, and I did not really spent much time to rearrange the pieces to more logical locations after.

I agree the class structure looks weird the shared parts between the two types could be extracted to a better location.

@bdewater
Copy link
Contributor Author

See #13

@bdewater bdewater closed this May 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants