gala 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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