gala 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +33 -0
- data/MIT-LICENSE +21 -0
- data/README.md +105 -0
- data/Rakefile +10 -0
- data/gala.gemspec +28 -0
- data/lib/gala.rb +1 -0
- data/lib/gala/payment_token.rb +122 -0
- data/lib/gala/resources/AppleRootCA-G3.pem +15 -0
- data/lib/gala/version.rb +3 -0
- data/notes.md +18 -0
- data/test/fixtures/certificate.pem +26 -0
- data/test/fixtures/private_key.pem +5 -0
- data/test/fixtures/token.json +10 -0
- data/test/payment_token_test.rb +62 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
data/gala.gemspec
ADDED
@@ -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
|
data/lib/gala.rb
ADDED
@@ -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-----
|
data/lib/gala/version.rb
ADDED
data/notes.md
ADDED
@@ -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,10 @@
|
|
1
|
+
{
|
2
|
+
"version":"EC_v1",
|
3
|
+
"data":"4OZho15e9Yp5K0EtKergKzeRpPAjnKHwmSNnagxhjwhKQ5d29sfTXjdbh1CtTJ4DYjsD6kfulNUnYmBTsruphBz7RRVI1WI8P0LrmfTnImjcq1mi+BRN7EtR2y6MkDmAr78anff91hlc+x8eWD/NpO/oZ1ey5qV5RBy/Jp5zh6ndVUVq8MHHhvQv4pLy5Tfi57Yo4RUhAsyXyTh4x/p1360BZmoWomK15NcJfUmoUCuwEYoi7xUkRwNr1z4MKnzMfneSRpUgdc0wADMeB6u1jcuwqQnnh2cusiagOTCfD6jO6tmouvu6KO54uU7bAbKz6cocIOEAOc6keyFXG5dfw8i3hJg6G2vIefHCwcKu1zFCHr4P7jLnYFDEhvxLm1KskDcuZeQHAkBMmLRSgj9NIcpBa94VN/JTga8W75IWAA==",
|
4
|
+
"signature":"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIID4jCCA4igAwIBAgIIJEPyqAad9XcwCgYIKoZIzj0EAwIwejEuMCwGA1UEAwwlQXBwbGUgQXBwbGljYXRpb24gSW50ZWdyYXRpb24gQ0EgLSBHMzEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTE0MDkyNTIyMDYxMVoXDTE5MDkyNDIyMDYxMVowXzElMCMGA1UEAwwcZWNjLXNtcC1icm9rZXItc2lnbl9VQzQtUFJPRDEUMBIGA1UECwwLaU9TIFN5c3RlbXMxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwhV37evWx7Ihj2jdcJChIY3HsL1vLCg9hGCV2Ur0pUEbg0IO2BHzQH6DMx8cVMP36zIg1rrV1O/0komJPnwPE6OCAhEwggINMEUGCCsGAQUFBwEBBDkwNzA1BggrBgEFBQcwAYYpaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwNC1hcHBsZWFpY2EzMDEwHQYDVR0OBBYEFJRX22/VdIGGiYl2L35XhQfnm1gkMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUI/JJxE+T5O8n5sT2KGw/orv9LkswggEdBgNVHSAEggEUMIIBEDCCAQwGCSqGSIb3Y2QFATCB/jCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA2BggrBgEFBQcCARYqaHR0cDovL3d3dy5hcHBsZS5jb20vY2VydGlmaWNhdGVhdXRob3JpdHkvMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwuYXBwbGUuY29tL2FwcGxlYWljYTMuY3JsMA4GA1UdDwEB/wQEAwIHgDAPBgkqhkiG92NkBh0EAgUAMAoGCCqGSM49BAMCA0gAMEUCIHKKnw+Soyq5mXQr1V62c0BXKpaHodYu9TWXEPUWPpbpAiEAkTecfW6+W5l0r0ADfzTCPq2YtbS39w01XIayqBNy8bEwggLuMIICdaADAgECAghJbS+/OpjalzAKBggqhkjOPQQDAjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0xNDA1MDYyMzQ2MzBaFw0yOTA1MDYyMzQ2MzBaMHoxLjAsBgNVBAMMJUFwcGxlIEFwcGxpY2F0aW9uIEludGVncmF0aW9uIENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPAXEYQZ12SF1RpeJYEHduiAou/ee65N4I38S5PhM1bVZls1riLQl3YNIk57ugj9dhfOiMt2u2ZwvsjoKYT/VEWjgfcwgfQwRgYIKwYBBQUHAQEEOjA4MDYGCCsGAQUFBzABhipodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDA0LWFwcGxlcm9vdGNhZzMwHQYDVR0OBBYEFCPyScRPk+TvJ+bE9ihsP6K7/S5LMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUu7DeoVgziJqkipnevr3rr9rLJKswNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5hcHBsZS5jb20vYXBwbGVyb290Y2FnMy5jcmwwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAg4EAgUAMAoGCCqGSM49BAMCA2cAMGQCMDrPcoNRFpmxhvs1w1bKYr/0F+3ZD3VNoo6+8ZyBXkK3ifiY95tZn5jVQQ2PnenC/gIwMi3VRCGwowV3bF3zODuQZ/0XfCwhbZZPxnJpghJvVPh6fRuZy5sJiSFhBpkPCZIdAAAxggFfMIIBWwIBATCBhjB6MS4wLAYDVQQDDCVBcHBsZSBBcHBsaWNhdGlvbiBJbnRlZ3JhdGlvbiBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMCCCRD8qgGnfV3MA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTQxMDI3MTk1MTQzWjAvBgkqhkiG9w0BCQQxIgQge01fe4e1+woRnaV3o8bZL7vmTLEDsnZfTQq+D7GYjnIwCgYIKoZIzj0EAwIERzBFAiEA5090eyrUE7pjWb8MqUeDp/vEY98vtrT0Uvre/66ccqQCICYe6cen516x/xsfi/tJr3SbTdxO25ZdN1bPH0Jiqgw7AAAAAAAA",
|
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
|