gala 0.3.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 282d6469414bc3815ece41d1ca0c88df2943c081
4
+ data.tar.gz: 1952197de23562117ce28641a8379c311190fbf3
5
+ SHA512:
6
+ metadata.gz: 8718b796da3467a4fea07e223a872f4a52daaab38241a0cd3cede1672653c18bc8577e6297f72d45aa65a22fcce064e0a7bc07fdadfd64c68087b82c8a927d27
7
+ data.tar.gz: a43e76f47d3d81c988221a5cd52a1cbb259cd9781860029984b5686df66948b3f39a00d7b3887eba5c0c4b931dfb6c0b1add7ae29ac472915518fa09751af4bb
@@ -0,0 +1,3 @@
1
+ .ruby-version
2
+ pkg/
3
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ gemspec
2
+
3
+ gem 'aead', git: 'https://github.com/Shopify/aead.git', ref: '340e7718d8bd9c1fcf3c443e32f439436ea2b70d'
@@ -0,0 +1,33 @@
1
+ GIT
2
+ remote: https://github.com/Shopify/aead.git
3
+ revision: 340e7718d8bd9c1fcf3c443e32f439436ea2b70d
4
+ ref: 340e7718d8bd9c1fcf3c443e32f439436ea2b70d
5
+ specs:
6
+ aead (1.8.2)
7
+ macaddr (~> 1)
8
+
9
+ PATH
10
+ remote: .
11
+ specs:
12
+ gala (0.3.1)
13
+ aead (~> 1.8)
14
+
15
+ GEM
16
+ remote: https://rubygems.org/
17
+ specs:
18
+ macaddr (1.7.1)
19
+ systemu (~> 2.6.2)
20
+ rake (12.0.0)
21
+ systemu (2.6.5)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ aead!
28
+ bundler (~> 1.14)
29
+ gala!
30
+ rake (~> 12.0)
31
+
32
+ BUNDLED WITH
33
+ 1.14.6
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2016] [Spreedly, Inc.]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ # Gala
2
+
3
+ Named after the [Gala apple](http://en.wikipedia.org/wiki/Gala_(apple)), Gala is a Ruby library for decrypting [Apple Pay payment tokens](https://developer.apple.com/library/ios/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html).
4
+
5
+ Gala is available under the MIT License.
6
+
7
+ ## Install
8
+
9
+ Add to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "gala", "~> 0.3.1"
13
+ ```
14
+
15
+ Or, if you need to track a development branch:
16
+
17
+ ```ruby
18
+ gem "gala", git: "https://github.com/spreedly/gala.git", ref: master
19
+ ```
20
+
21
+ Then `bundle install` to fetch Gala into your local environment.
22
+
23
+ ## Usage
24
+
25
+ Gala works by:
26
+
27
+ 1. Initializing an instance of `Gala::PaymentToken` with the hash of values present in the Apple Pay token string (a JSON representation of [this data](https://developer.apple.com/library/ios/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html)).
28
+ 2. Decrypting the token using the PEM formatted merchant certificate and private key (the latter of which, at least, is managed by a third-party such as a gateway or independent processor like [Spreedly](https://spreedly.com)).
29
+
30
+ ```ruby
31
+ require "gala"
32
+
33
+ # token_json = raw token string you get from your iOS app
34
+ token_attrs = JSON.parse(token_json)
35
+ token = Gala::PaymentToken.new(token_attrs)
36
+
37
+ certificate_pem = File.read("mycert.pem")
38
+ private_key_pem = File.read("private_key.pem")
39
+
40
+ decrypted_json = token.decrypt(certificate_pem, private_key_pem)
41
+ JSON.parse(decrypted_json)
42
+ # =>
43
+ {
44
+ "applicationPrimaryAccountNumber"=>"4109370251004320",
45
+ "applicationExpirationDate"=>"200731",
46
+ "currencyCode"=>"840",
47
+ "transactionAmount"=>100,
48
+ "deviceManufacturerIdentifier"=>"040010030273",
49
+ "paymentDataType"=>"3DSecure",
50
+ "paymentData"=> {
51
+ "onlinePaymentCryptogram"=>"Af9x/QwAA/DjmU65oyc1MAABAAA=",
52
+ "eciIndicator"=>"5"
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Testing
58
+
59
+ ```session
60
+ $ rake test
61
+ Started
62
+ ......
63
+
64
+ Finished in 0.017918 seconds.
65
+ ```
66
+
67
+ ## Releasing
68
+
69
+ To cut a new gem:
70
+
71
+ ### Setup RubyGems account
72
+
73
+ Make sure you have a [RubyGems account](https://rubygems.org) and have setup your local gem credentials with something like this:
74
+
75
+ ```bash
76
+ $ curl -u rwdaigle https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
77
+ <enter rubygems account password>
78
+ ```
79
+
80
+ If you are not yet listed as a gem owner, you will need to [request access](http://guides.rubygems.org/command-reference/#gem-owner) from @rwdaigle.
81
+
82
+ ### Release
83
+
84
+ Build and release the gem with (all changes should be committed and pushed to Github):
85
+
86
+ ```bash
87
+ $ rake release
88
+ ```
89
+
90
+ ## Changelog
91
+
92
+ ### v0.3.1
93
+
94
+ * Use Shopify aead library for compatibility w/ Ruby >= v2.2
95
+
96
+ ### v0.3.0
97
+
98
+ * Verify payment token signature
99
+
100
+ ## Contributors
101
+
102
+ * [dankimio](https://github.com/dankimio)
103
+ * [davidsantoso](https://github.com/davidsantoso)
104
+ * [mrezentes](https://github.com/mrezentes)
105
+ * [jnormore](https://github.com/jnormore)
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gala/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gala"
8
+ spec.version = Gala::VERSION
9
+ spec.authors = ["Mark Bennett", "Ryan Daigle"]
10
+ spec.email = ["ryan@spreedly.com"]
11
+
12
+ spec.summary = "Apple Pay payment token decryption library"
13
+ spec.description = "Given an (encrypted) Apple Pay token, verify and decrypt it"
14
+ spec.homepage = "https://github.com/spreedly/gala"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test)/}) }
18
+ spec.test_files = `git ls-files -- test/*`.split("\n")
19
+ spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.required_ruby_version = ">= 1.8.7"
23
+
24
+ spec.add_runtime_dependency 'aead', '~> 1.8'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.14'
27
+ spec.add_development_dependency 'rake', '~> 12.0'
28
+ end
@@ -0,0 +1 @@
1
+ require_relative "gala/payment_token"
@@ -0,0 +1,122 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'aead'
4
+
5
+ module Gala
6
+ class PaymentToken
7
+
8
+ MERCHANT_ID_FIELD_OID = "1.2.840.113635.100.6.32"
9
+ LEAF_CERTIFICATE_OID = "1.2.840.113635.100.6.29"
10
+ INTERMEDIATE_CERTIFICATE_OID = "1.2.840.113635.100.6.2.14"
11
+ APPLE_ROOT_CERT = File.read(File.dirname(__FILE__) + "/resources/AppleRootCA-G3.pem")
12
+
13
+ attr_accessor :version, :data, :signature, :transaction_id, :ephemeral_public_key,
14
+ :public_key_hash, :application_data
15
+
16
+ class MissingMerchantIdError < StandardError; end;
17
+ class InvalidSignatureError < StandardError; end;
18
+
19
+ def initialize(token_attrs)
20
+ self.version = token_attrs["version"]
21
+ self.data = token_attrs["data"]
22
+ self.signature = token_attrs["signature"]
23
+ headers = token_attrs["header"]
24
+ self.transaction_id = headers["transactionId"]
25
+ self.ephemeral_public_key = headers["ephemeralPublicKey"]
26
+ self.public_key_hash = headers["publicKeyHash"]
27
+ self.application_data = headers["applicationData"]
28
+ end
29
+
30
+ def decrypt(certificate_pem, private_key_pem)
31
+ self.class.validate_signature(signature, ephemeral_public_key, data, transaction_id, application_data)
32
+
33
+ certificate = OpenSSL::X509::Certificate.new(certificate_pem)
34
+ merchant_id = self.class.extract_merchant_id(certificate)
35
+ private_key = OpenSSL::PKey::EC.new(private_key_pem)
36
+ shared_secret = self.class.generate_shared_secret(private_key, ephemeral_public_key)
37
+ symmetric_key = self.class.generate_symmetric_key(merchant_id, shared_secret)
38
+
39
+ # Return JSON string, up to caller to parse
40
+ self.class.decrypt(Base64.decode64(data), symmetric_key)
41
+ end
42
+
43
+ class << self
44
+
45
+ def validate_signature(signature, ephemeral_public_key, data, transaction_id, application_data)
46
+ # Ensure that the certificates contain the correct custom OIDs
47
+ intermediate_cert = nil
48
+ leaf_cert = nil
49
+ p7 = OpenSSL::PKCS7.new(Base64.decode64(signature))
50
+ p7.certificates.each {|c|
51
+ c.extensions.each { |e|
52
+ leaf_cert = c if e.oid == LEAF_CERTIFICATE_OID
53
+ intermediate_cert = c if e.oid == INTERMEDIATE_CERTIFICATE_OID
54
+ }
55
+ }
56
+ raise InvalidSignatureError, "Signature does not contain the correct custom OIDs." unless leaf_cert && intermediate_cert
57
+
58
+ # Ensure that the root CA is the Apple Root CA - G3
59
+ root_cert = certificate = OpenSSL::X509::Certificate.new(APPLE_ROOT_CERT)
60
+
61
+ # Ensure that there is a valid X.509 chain of trust from the signature to the root CA
62
+ raise InvalidSignatureError, "Unable to verify a valid chain of trust from signature to root certificate." unless chain_of_trust_verified?(leaf_cert, intermediate_cert, root_cert)
63
+
64
+ #Ensure that the signature is a valid ECDSA signature
65
+ unless application_data
66
+ verification_string = Base64.decode64(ephemeral_public_key) + Base64.decode64(data) + [transaction_id].pack("H*")
67
+ # verification_string = verification_string + application_data.pack("H*") if application_data
68
+ store = OpenSSL::X509::Store.new
69
+ verified = p7.verify([], store, verification_string, OpenSSL::PKCS7::NOVERIFY )
70
+ raise InvalidSignatureError, "The given signature is not a valid ECDSA signature." unless verified
71
+ end
72
+ end
73
+
74
+ def chain_of_trust_verified?(leaf_cert, intermediate_cert, root_cert)
75
+ trusted_certificate_store = OpenSSL::X509::Store.new.tap do |store|
76
+ store.add_cert(root_cert)
77
+ store.add_cert(intermediate_cert)
78
+ end
79
+ trusted_certificate_store.verify(leaf_cert)
80
+ end
81
+
82
+ def extract_merchant_id(certificate)
83
+ merchant_id_field = certificate.extensions.find do |ext|
84
+ ext.oid == MERCHANT_ID_FIELD_OID
85
+ end
86
+ raise MissingMerchantIdError unless merchant_id_field
87
+ val = merchant_id_field.value
88
+ val[2..(val.length - 1)]
89
+ end
90
+
91
+ def generate_shared_secret(private_key, ephemeral_public_key)
92
+ public_ec = OpenSSL::PKey::EC.new(Base64.decode64(ephemeral_public_key))
93
+ point = OpenSSL::PKey::EC::Point.new(private_key.group, public_ec.public_key.to_bn)
94
+ private_key.dh_compute_key(point)
95
+ end
96
+
97
+ # Derive the symmetric key using the key derivation function described in NIST SP 800-56A, section 5.8.1
98
+ # http://csrc.nist.gov/publications/nistpubs/800-56A/SP800-56A_Revision1_Mar08-2007.pdf
99
+ def generate_symmetric_key(merchant_id, shared_secret)
100
+
101
+ kdf_algorithm = "\x0D" + 'id-aes256-GCM'
102
+ kdf_party_v = merchant_id.scan(/../).inject("") { |binary,hn| binary << hn.to_i(16).chr } # Converts each pair of hex characters into bytes in a string.
103
+ kdf_info = kdf_algorithm + "Apple" + kdf_party_v
104
+
105
+ digest = Digest::SHA256.new
106
+ digest << 0.chr * 3
107
+ digest << 1.chr
108
+ digest << shared_secret
109
+ digest << kdf_info
110
+ digest.digest
111
+ end
112
+
113
+ def decrypt(encrypted_data, symmetric_key)
114
+ init_length = 16
115
+ init_vector = 0.chr * init_length
116
+ mode = ::AEAD::Cipher.new('aes-256-gcm')
117
+ cipher = mode.new(symmetric_key, iv_len: init_length)
118
+ cipher.decrypt(init_vector, '', encrypted_data)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
3
+ QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
4
+ IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
5
+ MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
6
+ b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y
7
+ aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49
8
+ AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf
9
+ TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517
10
+ IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr
11
+ MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA
12
+ MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4
13
+ at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
14
+ 6BgD56KyKA==
15
+ -----END CERTIFICATE-----
@@ -0,0 +1,3 @@
1
+ module Gala
2
+ VERSION = "0.3.1" unless defined? Gala::VERSION
3
+ end
@@ -0,0 +1,18 @@
1
+ ## Generating the Apple Root Certificate
2
+
3
+ In order to get the certificate into a usable format, the following java code was used.
4
+ ```
5
+ InputStream inputStream = null;
6
+ X509Certificate appleRootCertificate = null;
7
+
8
+ try {
9
+ InputStream in = new FileInputStream(new File("/Users/mrezentes/Documents/workspace/AndroidPay/src/AppleRootCA-G3.cer"));
10
+ CertificateFactory certificateFactory = CertificateFactory.getInstance(X_509);
11
+ appleRootCertificate = (X509Certificate) certificateFactory.generateCertificate(in);
12
+ PEMWriter pw = new PEMWriter(new PrintWriter(System.out));
13
+ pw.writeObject(appleRootCertificate);
14
+ pw.flush();
15
+ pw.close();
16
+ }
17
+
18
+ ```
@@ -0,0 +1,26 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEcDCCBBagAwIBAgIIUyrEM4IzBHQwCgYIKoZIzj0EAwIwgYAxNDAyBgNVBAMM
3
+ K0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zIENBIC0gRzIxJjAk
4
+ BgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApB
5
+ cHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0xNDEwMjYxMjEwMTBaFw0xNjExMjQx
6
+ MjEwMTBaMIGhMS4wLAYKCZImiZPyLGQBAQwebWVyY2hhbnQuY29tLnNlYXRnZWVr
7
+ LlNlYXRHZWVrMTQwMgYDVQQDDCtNZXJjaGFudCBJRDogbWVyY2hhbnQuY29tLnNl
8
+ YXRnZWVrLlNlYXRHZWVrMRMwEQYDVQQLDAo5QjNRWTlXQlo1MRcwFQYDVQQKDA5T
9
+ ZWF0R2VlaywgSW5jLjELMAkGA1UEBhMCVVMwWTATBgcqhkjOPQIBBggqhkjOPQMB
10
+ BwNCAAQPjiA1kTEodST2wy5d5kQFrM0D5qBX9Ukry8W6D+vC7OqbMoTm/upRM1GR
11
+ HeA2LaVTrwAnpGhoO0ETqYF2Nu4Vo4ICVTCCAlEwRwYIKwYBBQUHAQEEOzA5MDcG
12
+ CCsGAQUFBzABhitodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDA0LWFwcGxld3dk
13
+ cmNhMjAxMB0GA1UdDgQWBBQWGfKgPgVBX8JOv84q1c04HShMmzAMBgNVHRMBAf8E
14
+ AjAAMB8GA1UdIwQYMBaAFIS2hMw6hmJyFlmU6BqjvUjfOt8LMIIBHQYDVR0gBIIB
15
+ FDCCARAwggEMBgkqhkiG92NkBQEwgf4wgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFu
16
+ Y2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2Nl
17
+ cHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5k
18
+ IGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRp
19
+ ZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wNgYIKwYBBQUHAgEWKmh0dHA6
20
+ Ly93d3cuYXBwbGUuY29tL2NlcnRpZmljYXRlYXV0aG9yaXR5LzA2BgNVHR8ELzAt
21
+ MCugKaAnhiVodHRwOi8vY3JsLmFwcGxlLmNvbS9hcHBsZXd3ZHJjYTIuY3JsMA4G
22
+ A1UdDwEB/wQEAwIDKDBPBgkqhkiG92NkBiAEQgxARjkzOEY0NjU4Q0EyQzFDOUMz
23
+ OEI4REZDQjVEQkIyQTIyNDU2MDdEREUyRjExNDYyMEU4NDY4RUY1MkQyMDhDQTAK
24
+ BggqhkjOPQQDAgNIADBFAiB+Q4zzpMj2DJTCIhDFBcmwK1zQAC70fY2IsYd8+Nxu
25
+ uwIhAKj9RrTOyiaQnoT5Mqi3UHopb6xTugl3LUDBloraBHyP
26
+ -----END CERTIFICATE-----
@@ -0,0 +1,5 @@
1
+ -----BEGIN EC PRIVATE KEY-----
2
+ MHcCAQEEIDqrpF0KEFW4Ncb76vyBi3StFLiT222sFC0wC3LsP1M9oAoGCCqGSM49
3
+ AwEHoUQDQgAED44gNZExKHUk9sMuXeZEBazNA+agV/VJK8vFug/rwuzqmzKE5v7q
4
+ UTNRkR3gNi2lU68AJ6RoaDtBE6mBdjbuFQ==
5
+ -----END EC PRIVATE KEY-----
@@ -0,0 +1,10 @@
1
+ {
2
+ "version":"EC_v1",
3
+ "data":"4OZho15e9Yp5K0EtKergKzeRpPAjnKHwmSNnagxhjwhKQ5d29sfTXjdbh1CtTJ4DYjsD6kfulNUnYmBTsruphBz7RRVI1WI8P0LrmfTnImjcq1mi+BRN7EtR2y6MkDmAr78anff91hlc+x8eWD/NpO/oZ1ey5qV5RBy/Jp5zh6ndVUVq8MHHhvQv4pLy5Tfi57Yo4RUhAsyXyTh4x/p1360BZmoWomK15NcJfUmoUCuwEYoi7xUkRwNr1z4MKnzMfneSRpUgdc0wADMeB6u1jcuwqQnnh2cusiagOTCfD6jO6tmouvu6KO54uU7bAbKz6cocIOEAOc6keyFXG5dfw8i3hJg6G2vIefHCwcKu1zFCHr4P7jLnYFDEhvxLm1KskDcuZeQHAkBMmLRSgj9NIcpBa94VN/JTga8W75IWAA==",
4
+ "signature":"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",
5
+ "header":{
6
+ "transactionId":"2686f5297f123ec7fd9d31074d43d201953ca75f098890375f13aed2737d92f2",
7
+ "ephemeralPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMwliotf2ICjiMwREdqyHSilqZzuV2fZey86nBIDlTY8sNMJv9CPpL5/DKg4bIEMe6qaj67mz4LWdr7Er0Ld5qA==",
8
+ "publicKeyHash":"LbsUwAT6w1JV9tFXocU813TCHks+LSuFF0R/eBkrWnQ="
9
+ }
10
+ }
@@ -0,0 +1,62 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+ require 'test/unit'
3
+ require 'json'
4
+ require 'gala'
5
+
6
+ class Gala::PaymentTokenTest < Test::Unit::TestCase
7
+
8
+ def setup
9
+ fixtures = File.dirname(__FILE__) + "/fixtures/"
10
+ @token_attrs = JSON.parse(File.read(fixtures + "token.json"))
11
+ @certificate = File.read(fixtures + "certificate.pem")
12
+ @private_key = File.read(fixtures + "private_key.pem")
13
+ @payment_token = Gala::PaymentToken.new(@token_attrs)
14
+ @merchant_id = "F938F4658CA2C1C9C38B8DFCB5DBB2A2245607DDE2F114620E8468EF52D208CA"
15
+ @shared_secret = Base64.decode64("a2pPfemSdA560FnzLSv8zfdlWdGJTonApOLq1zfgx8w=")
16
+ @symmetric_key = Base64.decode64("HOSago9Z1DhhukQvzmgpuCGPuwq1W0AgasMQWNZvUIY=")
17
+ end
18
+
19
+ def test_initialize
20
+ assert_equal @token_attrs["version"], @payment_token.version
21
+ assert_equal @token_attrs["data"], @payment_token.data
22
+ assert_equal @token_attrs["signature"], @payment_token.signature
23
+ assert_equal @token_attrs["header"]["transactionId"], @payment_token.transaction_id
24
+ assert_equal @token_attrs["header"]["ephemeralPublicKey"], @payment_token.ephemeral_public_key
25
+ assert_equal @token_attrs["header"]["publicKeyHash"], @payment_token.public_key_hash
26
+ end
27
+
28
+ def test_merchant_id
29
+ cert = OpenSSL::X509::Certificate.new(@certificate)
30
+ assert_equal @merchant_id, Gala::PaymentToken.extract_merchant_id(cert)
31
+ end
32
+
33
+ def test_shared_secret
34
+ priv_key = OpenSSL::PKey::EC.new(@private_key)
35
+ assert_equal @shared_secret, Gala::PaymentToken.generate_shared_secret(priv_key, @token_attrs["header"]["ephemeralPublicKey"])
36
+ end
37
+
38
+ def test_symmetric_key
39
+ assert_equal @symmetric_key, Gala::PaymentToken.generate_symmetric_key(@merchant_id, @shared_secret)
40
+ end
41
+
42
+ def test_decrypt
43
+ payment_data = JSON.parse(@payment_token.decrypt(@certificate, @private_key))
44
+ assert_equal "4109370251004320", payment_data["applicationPrimaryAccountNumber"]
45
+ assert_equal "200731", payment_data["applicationExpirationDate"]
46
+ assert_equal "840", payment_data["currencyCode"]
47
+ assert_equal 100, payment_data["transactionAmount"]
48
+ assert_equal nil, payment_data["cardholderName"]
49
+ assert_equal "040010030273", payment_data["deviceManufacturerIdentifier"]
50
+ assert_equal "3DSecure", payment_data["paymentDataType"]
51
+ assert_equal "Af9x/QwAA/DjmU65oyc1MAABAAA=", payment_data["paymentData"]["onlinePaymentCryptogram"]
52
+ assert_equal "5", payment_data["paymentData"]["eciIndicator"]
53
+ end
54
+
55
+ def test_failed_decrypt
56
+ @payment_token.data = "bogus4OZho15e9Yp5K0EtKergKzeRpPAjnKHwmSNnagxhjwhKQ5d29sfTXjdbh1CtTJ4DYjsD6kfulNUnYmBTsruphBz7RRVI1WI8P0LrmfTnImjcq1mi"
57
+ exception = assert_raise Gala::PaymentToken::InvalidSignatureError do
58
+ JSON.parse(@payment_token.decrypt(@certificate, @private_key))
59
+ end
60
+ assert_equal("The given signature is not a valid ECDSA signature.", exception.message)
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gala
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Mark Bennett
8
+ - Ryan Daigle
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2017-04-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aead
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.8'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.8'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.14'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.14'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '12.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '12.0'
56
+ description: Given an (encrypted) Apple Pay token, verify and decrypt it
57
+ email:
58
+ - ryan@spreedly.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - MIT-LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - gala.gemspec
70
+ - lib/gala.rb
71
+ - lib/gala/payment_token.rb
72
+ - lib/gala/resources/AppleRootCA-G3.pem
73
+ - lib/gala/version.rb
74
+ - notes.md
75
+ - test/fixtures/certificate.pem
76
+ - test/fixtures/private_key.pem
77
+ - test/fixtures/token.json
78
+ - test/payment_token_test.rb
79
+ homepage: https://github.com/spreedly/gala
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 1.8.7
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.2.5
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Apple Pay payment token decryption library
103
+ test_files:
104
+ - test/fixtures/certificate.pem
105
+ - test/fixtures/private_key.pem
106
+ - test/fixtures/token.json
107
+ - test/payment_token_test.rb