r2d2 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +46 -0
- data/.gitignore +6 -0
- data/Gemfile +5 -0
- data/README.md +120 -0
- data/Rakefile +10 -0
- data/lib/r2d2/payment_token.rb +82 -0
- data/lib/r2d2/version.rb +3 -0
- data/lib/r2d2.rb +3 -0
- data/r2d2.gemspec +27 -0
- data/test/fixtures/private_key.pem +5 -0
- data/test/fixtures/public_key.pem +4 -0
- data/test/fixtures/token.json +5 -0
- data/test/initial_dev/test_data.md +526 -0
- data/test/initial_dev/test_data_private_key.pem +5 -0
- data/test/initial_dev/test_data_token.json +5 -0
- data/test/payment_token_test.rb +49 -0
- data/test/test_helper.rb +5 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 026e3de4d7dca643f8f57f519a93014eaa3587ea
|
4
|
+
data.tar.gz: 1a58252e22b3595cf709d132b10308012baae046
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: abf1504ec9d6bbcfc77f774052e4c66d073033d2eb562edfe397711e03252e41e51ddf764be8c6abc14be455a4641b43cb1527b703ec1fb49def7a0a2f708b4e
|
7
|
+
data.tar.gz: 72bc939de0608e03740b44cfff6ba6941933ead007ba2c044b38f3c2e6964da749e8bdb355fad0b63658efbe4b4751f847fad37426363294c9de857d7fb9eb70
|
@@ -0,0 +1,46 @@
|
|
1
|
+
version: 2
|
2
|
+
jobs:
|
3
|
+
ruby-2.1:
|
4
|
+
docker:
|
5
|
+
- image: circleci/ruby:2.1.10
|
6
|
+
steps:
|
7
|
+
- checkout
|
8
|
+
- run: bundle
|
9
|
+
- run: rake test
|
10
|
+
ruby-2.2:
|
11
|
+
docker:
|
12
|
+
- image: circleci/ruby:2.2.10
|
13
|
+
steps:
|
14
|
+
- checkout
|
15
|
+
- run: bundle
|
16
|
+
- run: rake test
|
17
|
+
# ruby-2.3:
|
18
|
+
# docker:
|
19
|
+
# - image: circleci/ruby:2.3.7
|
20
|
+
# steps:
|
21
|
+
# - checkout
|
22
|
+
# - run: bundle
|
23
|
+
# - run: rake test
|
24
|
+
# ruby-2.4:
|
25
|
+
# docker:
|
26
|
+
# - image: circleci/ruby:2.4.4
|
27
|
+
# steps:
|
28
|
+
# - checkout
|
29
|
+
# - run: bundle
|
30
|
+
# - run: rake test
|
31
|
+
# ruby-2.5:
|
32
|
+
# docker:
|
33
|
+
# - image: circleci/ruby:2.5.1
|
34
|
+
# steps:
|
35
|
+
# - checkout
|
36
|
+
# - run: bundle
|
37
|
+
# - run: rake test
|
38
|
+
workflows:
|
39
|
+
version: 2
|
40
|
+
rubies:
|
41
|
+
jobs:
|
42
|
+
- ruby-2.1
|
43
|
+
- ruby-2.2
|
44
|
+
# - ruby-2.3
|
45
|
+
# - ruby-2.4
|
46
|
+
# - ruby-2.5
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# R2D2
|
2
|
+
|
3
|
+
[![CircleCI](https://circleci.com/gh/spreedly/r2d2.svg?style=svg)](https://circleci.com/gh/spreedly/r2d2)
|
4
|
+
|
5
|
+
R2D2 is a Ruby library for decrypting Android Pay payment tokens.
|
6
|
+
|
7
|
+
## Ruby support
|
8
|
+
|
9
|
+
Currently, only Ruby v2.2 and below are supported. For Ruby >= 2.3, work will need to be done (similar to [what was done in Gala](https://github.com/spreedly/gala/commit/0a4359ccdd5654b78747f9141645ca510ee255c2)) to use a compatible aead decryption algorithm.
|
10
|
+
|
11
|
+
## Install
|
12
|
+
|
13
|
+
Add to your `Gemfile`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem "android_pay", git: "https://github.com/spreedly/android_pay.git"
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
R2D2 takes input in the form of the hash of Android Pay token values:
|
22
|
+
|
23
|
+
```json
|
24
|
+
{
|
25
|
+
"encryptedMessage": "ZW5jcnlwdGVkTWVzc2FnZQ==",
|
26
|
+
"ephemeralPublicKey": "ZXBoZW1lcmFsUHVibGljS2V5",
|
27
|
+
"tag": "c2lnbmF0dXJl"
|
28
|
+
}
|
29
|
+
```
|
30
|
+
|
31
|
+
and the merchant's private key private key (which is managed by a third-party such as a gateway or independent processor like [Spreedly](https://spreedly.com)).
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require "android_pay"
|
35
|
+
|
36
|
+
# token_json = raw token string you get from Android Pay { "encryptedMessage": "...", "tag": "...", ...}
|
37
|
+
token_attrs = JSON.parse(token_json)
|
38
|
+
token = R2D2::PaymentToken.new(token_attrs)
|
39
|
+
|
40
|
+
private_key_pem = File.read("private_key.pem")
|
41
|
+
decrypted_json = token.decrypt(private_key_pem)
|
42
|
+
|
43
|
+
JSON.parse(decrypted_json)
|
44
|
+
# =>
|
45
|
+
{
|
46
|
+
“dpan”: “4444444444444444”,
|
47
|
+
“expirationMonth”: 10,
|
48
|
+
“expirationYear”: 2015 ,
|
49
|
+
“authMethod”: “3DS”,
|
50
|
+
“3dsCryptogram”: “AAAAAA...”,
|
51
|
+
“3dsEciIndicator”: “eci indicator”
|
52
|
+
}
|
53
|
+
```
|
54
|
+
|
55
|
+
### Performance
|
56
|
+
|
57
|
+
The library implements a constant time comparison algorithm for preventing timing attacks. The default pure ruby implementation is quite inefficient, but portable. If performance is a priority for you, you can use a faster comparison algorithm provided by the [fast_secure_compare](https://github.com/daxtens/fast_secure_compare).
|
58
|
+
|
59
|
+
To enable `FastSecureCompare` in your environment, add the following to your Gemfile:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
gem 'fast_secure_compare`
|
63
|
+
```
|
64
|
+
|
65
|
+
and require the extension in your application prior to loading r2d2:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
require 'fast_secure_compare/fast_secure_compare'
|
69
|
+
require 'r2d2/payment_token'
|
70
|
+
```
|
71
|
+
|
72
|
+
Benchmarks illustrating the overhead of the pure Ruby version:
|
73
|
+
|
74
|
+
```
|
75
|
+
user system total real
|
76
|
+
secure_compare 1.070000 0.010000 1.080000 ( 1.231714)
|
77
|
+
fast secure_compare 0.050000 0.000000 0.050000 ( 0.049753)
|
78
|
+
```
|
79
|
+
|
80
|
+
## Testing
|
81
|
+
|
82
|
+
```session
|
83
|
+
$ bundle exec rake
|
84
|
+
...
|
85
|
+
5 tests, 18 assertions, 0 failures, 0 errors, 0 skips
|
86
|
+
```
|
87
|
+
|
88
|
+
## Releasing
|
89
|
+
|
90
|
+
To cut a new gem:
|
91
|
+
|
92
|
+
### Setup RubyGems account
|
93
|
+
|
94
|
+
Make sure you have a [RubyGems account](https://rubygems.org) and have setup your local gem credentials with something like this:
|
95
|
+
|
96
|
+
```bash
|
97
|
+
$ curl -u rwdaigle https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
|
98
|
+
<enter rubygems account password>
|
99
|
+
```
|
100
|
+
|
101
|
+
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.
|
102
|
+
|
103
|
+
### Release
|
104
|
+
|
105
|
+
Build and release the gem with (all changes should be committed and pushed to Github):
|
106
|
+
|
107
|
+
```bash
|
108
|
+
$ rake release
|
109
|
+
```
|
110
|
+
|
111
|
+
## Changelog
|
112
|
+
|
113
|
+
### v0.1.2
|
114
|
+
|
115
|
+
* Setup CircleCI for more exhaustive Ruby version compatibility tests
|
116
|
+
* Add gem release instructions
|
117
|
+
|
118
|
+
## Contributors
|
119
|
+
|
120
|
+
* [methodmissing](https://github.com/methodmissing)
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
require 'hkdf'
|
4
|
+
|
5
|
+
module R2D2
|
6
|
+
class PaymentToken
|
7
|
+
|
8
|
+
attr_accessor :encrypted_message, :ephemeral_public_key, :tag
|
9
|
+
|
10
|
+
class TagVerificationError < StandardError; end;
|
11
|
+
|
12
|
+
def initialize(token_attrs)
|
13
|
+
self.ephemeral_public_key = token_attrs["ephemeralPublicKey"]
|
14
|
+
self.tag = token_attrs["tag"]
|
15
|
+
self.encrypted_message = token_attrs["encryptedMessage"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt(private_key_pem)
|
19
|
+
digest = OpenSSL::Digest.new('sha256')
|
20
|
+
private_key = OpenSSL::PKey::EC.new(private_key_pem)
|
21
|
+
|
22
|
+
shared_secret = self.class.generate_shared_secret(private_key, ephemeral_public_key)
|
23
|
+
|
24
|
+
# derive the symmetric_encryption_key and mac_key
|
25
|
+
hkdf_keys = self.class.derive_hkdf_keys(ephemeral_public_key, shared_secret);
|
26
|
+
|
27
|
+
# verify the tag is a valid value
|
28
|
+
self.class.verify_mac(digest, hkdf_keys[:mac_key], encrypted_message, tag)
|
29
|
+
|
30
|
+
self.class.decrypt_message(encrypted_message, hkdf_keys[:symmetric_encryption_key])
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
def generate_shared_secret(private_key, ephemeral_public_key)
|
36
|
+
ec = OpenSSL::PKey::EC.new('prime256v1')
|
37
|
+
bn = OpenSSL::BN.new(Base64.decode64(ephemeral_public_key), 2)
|
38
|
+
point = OpenSSL::PKey::EC::Point.new(ec.group, bn)
|
39
|
+
private_key.dh_compute_key(point)
|
40
|
+
end
|
41
|
+
|
42
|
+
def derive_hkdf_keys(ephemeral_public_key, shared_secret)
|
43
|
+
key_material = Base64.decode64(ephemeral_public_key) + shared_secret;
|
44
|
+
hkdf = HKDF.new(key_material, :algorithm => 'SHA256', :info => 'Android')
|
45
|
+
{
|
46
|
+
:symmetric_encryption_key => hkdf.next_bytes(16),
|
47
|
+
:mac_key => hkdf.next_bytes(16)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def verify_mac(digest, mac_key, encrypted_message, tag)
|
52
|
+
mac = OpenSSL::HMAC.digest(digest, mac_key, Base64.decode64(encrypted_message))
|
53
|
+
raise TagVerificationError unless secure_compare(mac, Base64.decode64(tag))
|
54
|
+
end
|
55
|
+
|
56
|
+
def decrypt_message(encrypted_data, symmetric_key)
|
57
|
+
decipher = OpenSSL::Cipher::AES128.new(:CTR)
|
58
|
+
decipher.decrypt
|
59
|
+
decipher.key = symmetric_key
|
60
|
+
decipher.auth_data = ""
|
61
|
+
decipher.update(Base64.decode64(encrypted_data)) + decipher.final
|
62
|
+
end
|
63
|
+
|
64
|
+
if defined?(FastSecureCompare)
|
65
|
+
def secure_compare(a, b)
|
66
|
+
FastSecureCompare.compare(a, b)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
# constant-time comparison algorithm to prevent timing attacks; borrowed from ActiveSupport::MessageVerifier
|
70
|
+
def secure_compare(a, b)
|
71
|
+
return false unless a.bytesize == b.bytesize
|
72
|
+
|
73
|
+
l = a.unpack("C#{a.bytesize}")
|
74
|
+
|
75
|
+
res = 0
|
76
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
77
|
+
res == 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/r2d2/version.rb
ADDED
data/lib/r2d2.rb
ADDED
data/r2d2.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'r2d2/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "r2d2"
|
6
|
+
s.version = R2D2::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Miki Rezentes", "Ryan Daigle"]
|
9
|
+
s.email = ["mrezentes@gmail.com", "ryan.daigle@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/spreedly/r2d2"
|
11
|
+
s.summary = "Android Pay payment token decryption library"
|
12
|
+
s.description = "Given an (encrypted) Android Pay token, verify and decrypt it"
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.required_ruby_version = ">= 1.8.7"
|
20
|
+
|
21
|
+
s.add_runtime_dependency 'hkdf'
|
22
|
+
|
23
|
+
s.add_development_dependency "bundler", "~> 1.15"
|
24
|
+
s.add_development_dependency "rake", "~> 12.0"
|
25
|
+
s.add_development_dependency "minitest", "~> 5.0"
|
26
|
+
s.add_development_dependency "pry-byebug"
|
27
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
{
|
2
|
+
"encryptedMessage": "V65NNwqzK0A1bi0F96HQZr4eFA8fWCatwykv3sFA8Cg4Wn4Ylk/szN6GiFTuYQFrHA7a/h0P3tfEQd09bor6pRqrM8/Bt12R0SHKtnQxbYxTjpMr/7C3Um79n0jseaPlK8+CHXljbYifwGB+cEFh/smP8IO1iw3TL/192HesutfVMKm9zpo5mLNzQ2GMU4JWUGIgrzsew6S6XshelrjE",
|
3
|
+
"ephemeralPublicKey": "BB9cOXHgf3KcY8dbsU6fhzqTJm3JFvzD+8wcWg0W9r+Xl5gYjoZRxHuYocAx3g82v2o0Le1E2w4sDDl5w3C0lmY=",
|
4
|
+
"tag": "boJLmOxDduTV5a34CO2IRbgxUjZ9WmfzxNl1lWqQ+Z0="
|
5
|
+
}
|
@@ -0,0 +1,526 @@
|
|
1
|
+
This file contains test data obtained from AndroidPay decryption code.
|
2
|
+
The values were used to confirm the steps of the processs were completing correctly.
|
3
|
+
This represents all needed info to confirm each step of the decryption process.
|
4
|
+
|
5
|
+
Note: for the HDFK step, the keying material is the ephemeral public key + the shared secret
|
6
|
+
|
7
|
+
|
8
|
+
```
|
9
|
+
{
|
10
|
+
"ephemeralPublicKey":"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE=",
|
11
|
+
"encryptedMessage":"PHxZxBQvVWwP",
|
12
|
+
"tag":"s9wa3Q2WiyGi/eDA4XYVklq08KZiSxB7xvRiKK3H7kE="
|
13
|
+
}
|
14
|
+
```
|
15
|
+
```
|
16
|
+
-----BEGIN EC PRIVATE KEY-----
|
17
|
+
MHcCAQEEIAj0rha+IkiGkKa443IRz8g7tjVxXtLwwaE6T5GGivXXoAoGCCqGSM49
|
18
|
+
AwEHoUQDQgAE52hc/70CrjvdKcbCDclTVqI2mx328faiCerg+0O2UsZrcqPxM9/6
|
19
|
+
NpA0/INKSHclSGJLQrKuuVaECA1kodgXZg==
|
20
|
+
-----END EC PRIVATE KEY-----
|
21
|
+
```
|
22
|
+
|
23
|
+
Keys include the byte array first, then the hex underneath
|
24
|
+
|
25
|
+
encryptionKey
|
26
|
+
|
27
|
+
```
|
28
|
+
[16, 79, 44, -105, -90, -108, -104, -111, 8, 18, -18, -101, 121, 100, -109, 98]
|
29
|
+
104F2C97A69498910812EE9B79649362
|
30
|
+
```
|
31
|
+
|
32
|
+
macKey
|
33
|
+
|
34
|
+
```
|
35
|
+
[-28, -1, 49, -64, 121, -124, -9, 98, -89, 11, -22, 96, -37, -118, -47, 28]
|
36
|
+
E4FF31C07984F762A70BEA60DB8AD11C
|
37
|
+
```
|
38
|
+
|
39
|
+
sharedKey
|
40
|
+
|
41
|
+
```
|
42
|
+
[16, 79, 44, -105, -90, -108, -104, -111, 8, 18, -18, -101, 121, 100, -109, 98, -28, -1, 49, -64, 121, -124, -9, 98, -89, 11, -22, 96, -37, -118, -47, 28]
|
43
|
+
104F2C97A69498910812EE9B79649362E4FF31C07984F762A70BEA60DB8AD11C
|
44
|
+
```
|
45
|
+
|
46
|
+
Java class for decryption
|
47
|
+
|
48
|
+
```
|
49
|
+
import org.bouncycastle.crypto.digests.SHA256Digest;
|
50
|
+
|
51
|
+
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
|
52
|
+
|
53
|
+
import org.bouncycastle.crypto.params.HKDFParameters;
|
54
|
+
|
55
|
+
import org.bouncycastle.jce.ECNamedCurveTable;
|
56
|
+
|
57
|
+
import org.bouncycastle.jce.ECPointUtil;
|
58
|
+
|
59
|
+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
60
|
+
|
61
|
+
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
|
62
|
+
|
63
|
+
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
|
64
|
+
|
65
|
+
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
|
66
|
+
|
67
|
+
import org.bouncycastle.util.encoders.Base64;
|
68
|
+
|
69
|
+
import org.bouncycastle.util.encoders.Hex;
|
70
|
+
|
71
|
+
import org.json.JSONException;
|
72
|
+
|
73
|
+
import org.json.JSONObject;
|
74
|
+
|
75
|
+
import java.io.IOException;
|
76
|
+
import java.io.PrintWriter;
|
77
|
+
import java.nio.charset.Charset;
|
78
|
+
|
79
|
+
import java.security.InvalidAlgorithmParameterException;
|
80
|
+
|
81
|
+
import java.security.InvalidKeyException;
|
82
|
+
|
83
|
+
import java.security.KeyFactory;
|
84
|
+
|
85
|
+
import java.security.NoSuchAlgorithmException;
|
86
|
+
|
87
|
+
import java.security.NoSuchProviderException;
|
88
|
+
|
89
|
+
import java.security.PrivateKey;
|
90
|
+
|
91
|
+
import java.security.PublicKey;
|
92
|
+
|
93
|
+
import java.security.Security;
|
94
|
+
|
95
|
+
import java.security.spec.ECParameterSpec;
|
96
|
+
|
97
|
+
import java.security.spec.ECPublicKeySpec;
|
98
|
+
|
99
|
+
import java.security.spec.InvalidKeySpecException;
|
100
|
+
|
101
|
+
import java.security.spec.PKCS8EncodedKeySpec;
|
102
|
+
import java.util.Arrays;
|
103
|
+
|
104
|
+
import javax.crypto.BadPaddingException;
|
105
|
+
|
106
|
+
import javax.crypto.Cipher;
|
107
|
+
|
108
|
+
import javax.crypto.IllegalBlockSizeException;
|
109
|
+
|
110
|
+
import javax.crypto.KeyAgreement;
|
111
|
+
|
112
|
+
import javax.crypto.Mac;
|
113
|
+
|
114
|
+
import javax.crypto.NoSuchPaddingException;
|
115
|
+
|
116
|
+
import javax.crypto.spec.IvParameterSpec;
|
117
|
+
|
118
|
+
import javax.crypto.spec.SecretKeySpec;
|
119
|
+
|
120
|
+
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
121
|
+
|
122
|
+
/** Utility for decrypting encrypted network tokens as per Android Pay InApp
|
123
|
+
|
124
|
+
spec. */
|
125
|
+
|
126
|
+
class NetworkTokenDecryptionUtil {
|
127
|
+
|
128
|
+
private static final String SECURITY_PROVIDER = "BC";
|
129
|
+
|
130
|
+
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
131
|
+
|
132
|
+
private static final String ASYMMETRIC_KEY_TYPE = "EC";
|
133
|
+
|
134
|
+
private static final String KEY_AGREEMENT_ALGORITHM = "ECDH";
|
135
|
+
|
136
|
+
/** OpenSSL name of the NIST P126 Elliptic Curve */
|
137
|
+
|
138
|
+
private static final String EC_CURVE = "prime256v1";
|
139
|
+
|
140
|
+
private static final String SYMMETRIC_KEY_TYPE = "AES";
|
141
|
+
|
142
|
+
private static final String SYMMETRIC_ALGORITHM = "AES/CTR/NoPadding";
|
143
|
+
|
144
|
+
private static final byte[] SYMMETRIC_IV = Hex.decode("00000000000000000000000000000000");
|
145
|
+
|
146
|
+
private static final int SYMMETRIC_KEY_BYTE_COUNT = 16;
|
147
|
+
|
148
|
+
private static final String MAC_ALGORITHM = "HmacSHA256";
|
149
|
+
|
150
|
+
private static final int MAC_KEY_BYTE_COUNT = 16;
|
151
|
+
|
152
|
+
private static final byte[] HKDF_INFO = "Android".getBytes(DEFAULT_CHARSET);
|
153
|
+
|
154
|
+
private static final byte[] HKDF_SALT = null /* equivalent to a zeroed salt of hashLen */;
|
155
|
+
|
156
|
+
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
157
|
+
|
158
|
+
private PrivateKey privateKey;
|
159
|
+
|
160
|
+
private NetworkTokenDecryptionUtil(PrivateKey privateKey) {
|
161
|
+
|
162
|
+
if (!ASYMMETRIC_KEY_TYPE.equals(privateKey.getAlgorithm())) {
|
163
|
+
|
164
|
+
throw new IllegalArgumentException("Unexpected type of private key");
|
165
|
+
|
166
|
+
}
|
167
|
+
|
168
|
+
this.privateKey = privateKey;
|
169
|
+
|
170
|
+
}
|
171
|
+
|
172
|
+
public static NetworkTokenDecryptionUtil createFromPkcs8EncodedPrivateKey(byte[] pkcs8PrivateKey) {
|
173
|
+
|
174
|
+
PrivateKey privateKey = null;
|
175
|
+
|
176
|
+
try {
|
177
|
+
|
178
|
+
KeyFactory asymmetricKeyFactory = KeyFactory.getInstance(ASYMMETRIC_KEY_TYPE, SECURITY_PROVIDER);
|
179
|
+
|
180
|
+
privateKey = asymmetricKeyFactory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8PrivateKey));
|
181
|
+
// System.out.println("Private key");
|
182
|
+
// System.out.println(privateKey);
|
183
|
+
// JcaPEMWriter writer = new JcaPEMWriter(new PrintWriter(System.out));
|
184
|
+
// writer.writeObject(privateKey);
|
185
|
+
// writer.close();
|
186
|
+
|
187
|
+
} catch (NoSuchAlgorithmException | NoSuchProviderException |
|
188
|
+
|
189
|
+
InvalidKeySpecException e) {
|
190
|
+
|
191
|
+
throw new RuntimeException("Failed to create NetworkTokenDecryptionUtil", e);
|
192
|
+
|
193
|
+
}
|
194
|
+
|
195
|
+
return new NetworkTokenDecryptionUtil(privateKey);
|
196
|
+
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
|
201
|
+
* Sets up the {@link #SECURITY_PROVIDER} if not yet set up.
|
202
|
+
|
203
|
+
*
|
204
|
+
|
205
|
+
* <p>You must call this method at least once before using this class.
|
206
|
+
|
207
|
+
*/
|
208
|
+
|
209
|
+
public static void setupSecurityProviderIfNecessary() {
|
210
|
+
|
211
|
+
if (Security.getProvider(SECURITY_PROVIDER) == null) {
|
212
|
+
|
213
|
+
Security.addProvider(new BouncyCastleProvider());
|
214
|
+
|
215
|
+
}
|
216
|
+
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
|
221
|
+
* Verifies then decrypts the given payload according to the Android Pay
|
222
|
+
|
223
|
+
Network Token
|
224
|
+
|
225
|
+
* encryption spec.
|
226
|
+
|
227
|
+
*/
|
228
|
+
|
229
|
+
public String verifyThenDecrypt(String encryptedPayloadJson) {
|
230
|
+
|
231
|
+
try {
|
232
|
+
|
233
|
+
JSONObject object = new JSONObject(encryptedPayloadJson);
|
234
|
+
|
235
|
+
byte[] ephemeralPublicKeyBytes = Base64.decode(object.getString("ephemeralPublicKey"));
|
236
|
+
System.out.println("ephemeralPublicKey");
|
237
|
+
System.out.println(object.getString("ephemeralPublicKey"));
|
238
|
+
System.out.println("encryptedMessage");
|
239
|
+
System.out.println(object.getString("encryptedMessage"));
|
240
|
+
System.out.println("tag");
|
241
|
+
System.out.println(object.getString("tag"));
|
242
|
+
byte[] encryptedMessage = Base64.decode(object.getString("encryptedMessage"));
|
243
|
+
|
244
|
+
byte[] tag = Base64.decode(object.getString("tag"));
|
245
|
+
|
246
|
+
// Parsing public key.
|
247
|
+
|
248
|
+
ECParameterSpec asymmetricKeyParams = generateECParameterSpec();
|
249
|
+
|
250
|
+
KeyFactory asymmetricKeyFactory = KeyFactory.getInstance(ASYMMETRIC_KEY_TYPE, SECURITY_PROVIDER);
|
251
|
+
|
252
|
+
PublicKey ephemeralPublicKey = asymmetricKeyFactory.generatePublic(new ECPublicKeySpec( ECPointUtil.decodePoint(asymmetricKeyParams.getCurve(), ephemeralPublicKeyBytes), asymmetricKeyParams));
|
253
|
+
|
254
|
+
// Deriving shared secret.
|
255
|
+
|
256
|
+
KeyAgreement keyAgreement = KeyAgreement.getInstance(KEY_AGREEMENT_ALGORITHM,SECURITY_PROVIDER);
|
257
|
+
|
258
|
+
keyAgreement.init(privateKey);
|
259
|
+
|
260
|
+
keyAgreement.doPhase(ephemeralPublicKey, true);
|
261
|
+
|
262
|
+
byte[] sharedSecret = keyAgreement.generateSecret();
|
263
|
+
System.out.println("Shared Secret Byte Array");
|
264
|
+
System.out.println(Arrays.toString(sharedSecret));
|
265
|
+
char[] hexChars = new char[sharedSecret.length * 2];
|
266
|
+
for ( int j = 0; j < sharedSecret.length; j++ ) {
|
267
|
+
int v = sharedSecret[j] & 0xFF;
|
268
|
+
hexChars[j * 2] = hexArray[v >>> 4];
|
269
|
+
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
270
|
+
}
|
271
|
+
System.out.println("Shared Secret Hex Array");
|
272
|
+
System.out.println(hexChars);
|
273
|
+
|
274
|
+
|
275
|
+
// Deriving encryption and mac keys.
|
276
|
+
|
277
|
+
HKDFBytesGenerator hkdfBytesGenerator = new HKDFBytesGenerator(new SHA256Digest());
|
278
|
+
|
279
|
+
byte[] khdfInput = ByteUtils.concatenate(ephemeralPublicKeyBytes, sharedSecret);
|
280
|
+
|
281
|
+
hkdfBytesGenerator.init(new HKDFParameters(khdfInput, HKDF_SALT, HKDF_INFO));
|
282
|
+
byte[] sharedKey = new byte[SYMMETRIC_KEY_BYTE_COUNT * 2];
|
283
|
+
hkdfBytesGenerator.generateBytes(sharedKey, 0, SYMMETRIC_KEY_BYTE_COUNT*2);
|
284
|
+
System.out.println("sharedKey");
|
285
|
+
System.out.println(Arrays.toString(sharedKey));
|
286
|
+
char[] sharedChars = new char[sharedKey.length * 2];
|
287
|
+
for ( int j = 0; j < sharedKey.length; j++ ) {
|
288
|
+
int v = sharedKey[j] & 0xFF;
|
289
|
+
sharedChars[j * 2] = hexArray[v >>> 4];
|
290
|
+
sharedChars[j * 2 + 1] = hexArray[v & 0x0F];
|
291
|
+
}
|
292
|
+
System.out.println(sharedChars);
|
293
|
+
|
294
|
+
byte[] encryptionKey = new byte[SYMMETRIC_KEY_BYTE_COUNT];
|
295
|
+
|
296
|
+
hkdfBytesGenerator.generateBytes(encryptionKey, 0, SYMMETRIC_KEY_BYTE_COUNT);
|
297
|
+
System.out.println("encryptionKey");
|
298
|
+
System.out.println(Arrays.toString(encryptionKey));
|
299
|
+
char[] encryptChars = new char[encryptionKey.length * 2];
|
300
|
+
for ( int j = 0; j < encryptionKey.length; j++ ) {
|
301
|
+
int v = encryptionKey[j] & 0xFF;
|
302
|
+
encryptChars[j * 2] = hexArray[v >>> 4];
|
303
|
+
encryptChars[j * 2 + 1] = hexArray[v & 0x0F];
|
304
|
+
}
|
305
|
+
System.out.println(encryptChars);
|
306
|
+
|
307
|
+
byte[] macKey = new byte[MAC_KEY_BYTE_COUNT];
|
308
|
+
hkdfBytesGenerator.generateBytes(macKey, 0, MAC_KEY_BYTE_COUNT);
|
309
|
+
System.out.println("macKey");
|
310
|
+
System.out.println(Arrays.toString(macKey));
|
311
|
+
char[] macChars = new char[macKey.length * 2];
|
312
|
+
for ( int j = 0; j < macKey.length; j++ ) {
|
313
|
+
int v = macKey[j] & 0xFF;
|
314
|
+
macChars[j * 2] = hexArray[v >>> 4];
|
315
|
+
macChars[j * 2 + 1] = hexArray[v & 0x0F];
|
316
|
+
}
|
317
|
+
System.out.println(macChars);
|
318
|
+
|
319
|
+
|
320
|
+
|
321
|
+
// Verifying Message Authentication Code (aka mac/tag)
|
322
|
+
|
323
|
+
Mac macGenerator = Mac.getInstance(MAC_ALGORITHM, SECURITY_PROVIDER);
|
324
|
+
|
325
|
+
macGenerator.init(new SecretKeySpec(macKey, MAC_ALGORITHM));
|
326
|
+
|
327
|
+
byte[] expectedTag = macGenerator.doFinal(encryptedMessage);
|
328
|
+
|
329
|
+
if (!isArrayEqual(tag, expectedTag)) {
|
330
|
+
|
331
|
+
throw new RuntimeException("Bad Message Authentication Code!");
|
332
|
+
|
333
|
+
}
|
334
|
+
|
335
|
+
// Decrypting the message.
|
336
|
+
|
337
|
+
Cipher cipher = Cipher.getInstance(SYMMETRIC_ALGORITHM);
|
338
|
+
|
339
|
+
cipher.init( Cipher.DECRYPT_MODE, new SecretKeySpec(encryptionKey, SYMMETRIC_KEY_TYPE), new IvParameterSpec(SYMMETRIC_IV));
|
340
|
+
|
341
|
+
return new String(cipher.doFinal(encryptedMessage), DEFAULT_CHARSET);
|
342
|
+
|
343
|
+
} catch (JSONException | NoSuchAlgorithmException |NoSuchProviderException| InvalidKeySpecException | InvalidKeyException |NoSuchPaddingException
|
344
|
+
|
345
|
+
| InvalidAlgorithmParameterException | IllegalBlockSizeException |BadPaddingException e) {
|
346
|
+
|
347
|
+
throw new RuntimeException("Failed verifying/decrypting message", e);
|
348
|
+
|
349
|
+
}
|
350
|
+
|
351
|
+
}
|
352
|
+
|
353
|
+
private ECNamedCurveSpec generateECParameterSpec() {
|
354
|
+
|
355
|
+
ECNamedCurveParameterSpec bcParams =ECNamedCurveTable.getParameterSpec(EC_CURVE);
|
356
|
+
|
357
|
+
ECNamedCurveSpec params = new ECNamedCurveSpec(bcParams.getName(),bcParams.getCurve(),bcParams.getG(), bcParams.getN(), bcParams.getH(),bcParams.getSeed());
|
358
|
+
|
359
|
+
return params;
|
360
|
+
|
361
|
+
}
|
362
|
+
|
363
|
+
/**
|
364
|
+
|
365
|
+
* Fixedtiming array comparison.
|
366
|
+
|
367
|
+
*/
|
368
|
+
|
369
|
+
public static boolean isArrayEqual(byte[] a, byte[] b) {
|
370
|
+
|
371
|
+
if (a.length != b.length) {
|
372
|
+
|
373
|
+
return false;
|
374
|
+
|
375
|
+
}
|
376
|
+
|
377
|
+
int result = 0;
|
378
|
+
|
379
|
+
for (int i = 0; i < a.length; i++) {
|
380
|
+
|
381
|
+
result |= a[i] ^ b[i];
|
382
|
+
|
383
|
+
}
|
384
|
+
|
385
|
+
return result == 0;
|
386
|
+
|
387
|
+
}
|
388
|
+
|
389
|
+
}
|
390
|
+
```
|
391
|
+
|
392
|
+
Java Test class
|
393
|
+
|
394
|
+
```
|
395
|
+
import static org.junit.Assert.assertEquals;import static org.junit.Assert.fail;
|
396
|
+
|
397
|
+
import com.google.common.io.BaseEncoding;
|
398
|
+
|
399
|
+
import org.bouncycastle.util.encoders.Base64;
|
400
|
+
|
401
|
+
import org.json.JSONObject;
|
402
|
+
|
403
|
+
import org.junit.Before;
|
404
|
+
|
405
|
+
import org.junit.Test;
|
406
|
+
|
407
|
+
import org.junit.runner.RunWith;
|
408
|
+
|
409
|
+
import org.junit.runners.JUnit4;
|
410
|
+
|
411
|
+
/** Unit tests for {@link NetworkTokenDecryptionUtil}. */
|
412
|
+
|
413
|
+
@RunWith(JUnit4.class)
|
414
|
+
|
415
|
+
public class NetworkTokenDecryptionUtilTest {
|
416
|
+
|
417
|
+
/**
|
418
|
+
|
419
|
+
* Created with:
|
420
|
+
|
421
|
+
* <pre>
|
422
|
+
|
423
|
+
* openssl pkcs8 topk8 inform PEM outform PEM in merchantkey.pem nocrypt
|
424
|
+
|
425
|
+
* </pre>
|
426
|
+
|
427
|
+
*/
|
428
|
+
|
429
|
+
private static final String MERCHANT_PRIVATE_KEY_PKCS8_BASE64 =
|
430
|
+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCPSuFr4iSIaQprjj" +
|
431
|
+
|
432
|
+
"chHPyDu2NXFe0vDBoTpPkYaK9dehRANCAATnaFz/vQKuO90pxsINyVNWojabHfbx" +
|
433
|
+
|
434
|
+
"9qIJ6uD7Q7ZSxmtyo/Ez3/o2kDT8g0pIdyVIYktCsq65VoQIDWSh2Bdm";
|
435
|
+
|
436
|
+
private static final String ENCRYPTED_PAYLOAD = "{"
|
437
|
+
|
438
|
+
+ "\"encryptedMessage\":\"PHxZxBQvVWwP\","
|
439
|
+
|
440
|
+
+ "\"ephemeralPublicKey\":\"BPhVspn70Zj2Kkgu9t8+ApEuUWsI\\/zos5whGCQBlgOkuYagOis7qsrcbQrcpr"
|
441
|
+
|
442
|
+
+ "jvTZO3XOU+Qbcc28FSgsRtcgQE=\","
|
443
|
+
|
444
|
+
+ "\"tag\":\"TNwa3Q2WiyGi\\/eDA4XYVklq08KZiSxB7xvRiKK3H7kE=\"}";
|
445
|
+
|
446
|
+
private NetworkTokenDecryptionUtil util;
|
447
|
+
|
448
|
+
@Before
|
449
|
+
public void setUp() {
|
450
|
+
|
451
|
+
NetworkTokenDecryptionUtil.setupSecurityProviderIfNecessary();
|
452
|
+
System.out.println("Merchant private key array");
|
453
|
+
System.out.println(MERCHANT_PRIVATE_KEY_PKCS8_BASE64);
|
454
|
+
|
455
|
+
util = NetworkTokenDecryptionUtil.createFromPkcs8EncodedPrivateKey( BaseEncoding.base64().decode(MERCHANT_PRIVATE_KEY_PKCS8_BASE64));
|
456
|
+
|
457
|
+
}
|
458
|
+
|
459
|
+
@Test
|
460
|
+
public void testShouldDecrypt() {
|
461
|
+
|
462
|
+
assertEquals("plaintext", util.verifyThenDecrypt(ENCRYPTED_PAYLOAD));
|
463
|
+
|
464
|
+
}
|
465
|
+
|
466
|
+
@Test
|
467
|
+
public void testShouldFailIfBadTag() throws Exception {
|
468
|
+
|
469
|
+
JSONObject payload = new JSONObject(ENCRYPTED_PAYLOAD);
|
470
|
+
|
471
|
+
byte[] tag = Base64.decode(payload.getString("tag"));
|
472
|
+
// Messing with the first byte
|
473
|
+
|
474
|
+
tag[0] = (byte) ~tag[0];
|
475
|
+
|
476
|
+
payload.put("tag", new String(Base64.encode(tag)));
|
477
|
+
|
478
|
+
try {
|
479
|
+
|
480
|
+
util.verifyThenDecrypt(payload.toString());
|
481
|
+
|
482
|
+
fail();
|
483
|
+
|
484
|
+
} catch (RuntimeException e) {
|
485
|
+
|
486
|
+
assertEquals("Bad Message Authentication Code!", e.getMessage());
|
487
|
+
|
488
|
+
}
|
489
|
+
}
|
490
|
+
|
491
|
+
@Test
|
492
|
+
|
493
|
+
public void testShouldFailIfEncryptedMessageWasChanged() throws Exception {
|
494
|
+
|
495
|
+
JSONObject payload = new JSONObject(ENCRYPTED_PAYLOAD);
|
496
|
+
|
497
|
+
byte[] encryptedMessage =
|
498
|
+
|
499
|
+
Base64.decode(payload.getString("encryptedMessage"));
|
500
|
+
|
501
|
+
// Messing with the first byte
|
502
|
+
|
503
|
+
encryptedMessage[0] = (byte) ~encryptedMessage[0];
|
504
|
+
|
505
|
+
payload.put("encryptedMessage", new
|
506
|
+
|
507
|
+
String(Base64.encode(encryptedMessage)));
|
508
|
+
|
509
|
+
try {
|
510
|
+
|
511
|
+
util.verifyThenDecrypt(payload.toString());
|
512
|
+
|
513
|
+
fail();
|
514
|
+
|
515
|
+
} catch (RuntimeException e) {
|
516
|
+
|
517
|
+
assertEquals("Bad Message Authentication Code!", e.getMessage());
|
518
|
+
|
519
|
+
}
|
520
|
+
}
|
521
|
+
}
|
522
|
+
```
|
523
|
+
|
524
|
+
|
525
|
+
|
526
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class R2D2::PaymentTokenTest < Minitest::Test
|
4
|
+
|
5
|
+
def setup
|
6
|
+
fixtures = File.dirname(__FILE__) + "/fixtures/"
|
7
|
+
@token_attrs = JSON.parse(File.read(fixtures + "token.json"))
|
8
|
+
@private_key = File.read(fixtures + "private_key.pem")
|
9
|
+
@payment_token = R2D2::PaymentToken.new(@token_attrs)
|
10
|
+
@shared_secret = ['44a9715c18ebcb255af705f7332657420aca40604334a7d48a89baba18280a97']
|
11
|
+
@mac_key = ["d8976b95c980760d8ce3933994c6eda1"]
|
12
|
+
@symmetric_encryption_key = ["c7b2670dc0630edd0a9101dd5d70e4b2"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initialize
|
16
|
+
assert_equal @token_attrs["ephemeralPublicKey"], @payment_token.ephemeral_public_key
|
17
|
+
assert_equal @token_attrs["tag"], @payment_token.tag
|
18
|
+
assert_equal @token_attrs["encryptedMessage"], @payment_token.encrypted_message
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_successful_decrypt
|
22
|
+
payment_data = JSON.parse(@payment_token.decrypt( @private_key))
|
23
|
+
assert_equal "4895370012003478", payment_data["dpan"]
|
24
|
+
assert_equal 12, payment_data["expirationMonth"]
|
25
|
+
assert_equal 2020, payment_data["expirationYear"]
|
26
|
+
assert_equal "3DS", payment_data["authMethod"]
|
27
|
+
assert_equal "AgAAAAAABk4DWZ4C28yUQAAAAAA=", payment_data["3dsCryptogram"]
|
28
|
+
assert_equal "07", payment_data["3dsEciIndicator"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_shared_secret
|
32
|
+
priv_key = OpenSSL::PKey::EC.new(@private_key)
|
33
|
+
assert_equal @shared_secret, R2D2::PaymentToken.generate_shared_secret(priv_key, @payment_token.ephemeral_public_key).unpack('H*')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_derive_hkdf_keys
|
37
|
+
hkdf_keys = R2D2::PaymentToken.derive_hkdf_keys(@payment_token.ephemeral_public_key, @shared_secret[0])
|
38
|
+
assert_equal hkdf_keys[:symmetric_encryption_key].unpack('H*'), @symmetric_encryption_key
|
39
|
+
assert_equal hkdf_keys[:mac_key].unpack('H*'), @mac_key
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_invalid_tag
|
43
|
+
@payment_token.tag = "SomethingBogus"
|
44
|
+
assert_raises R2D2::PaymentToken::TagVerificationError do
|
45
|
+
JSON.parse(@payment_token.decrypt( @private_key))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: r2d2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Miki Rezentes
|
8
|
+
- Ryan Daigle
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-05-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: hkdf
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
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.15'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.15'
|
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
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: minitest
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '5.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '5.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: pry-byebug
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
description: Given an (encrypted) Android Pay token, verify and decrypt it
|
85
|
+
email:
|
86
|
+
- mrezentes@gmail.com
|
87
|
+
- ryan.daigle@gmail.com
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".circleci/config.yml"
|
93
|
+
- ".gitignore"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/r2d2.rb
|
98
|
+
- lib/r2d2/payment_token.rb
|
99
|
+
- lib/r2d2/version.rb
|
100
|
+
- r2d2.gemspec
|
101
|
+
- test/fixtures/private_key.pem
|
102
|
+
- test/fixtures/public_key.pem
|
103
|
+
- test/fixtures/token.json
|
104
|
+
- test/initial_dev/test_data.md
|
105
|
+
- test/initial_dev/test_data_private_key.pem
|
106
|
+
- test/initial_dev/test_data_token.json
|
107
|
+
- test/payment_token_test.rb
|
108
|
+
- test/test_helper.rb
|
109
|
+
homepage: https://github.com/spreedly/r2d2
|
110
|
+
licenses: []
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 1.8.7
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.6.11
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Android Pay payment token decryption library
|
132
|
+
test_files:
|
133
|
+
- test/fixtures/private_key.pem
|
134
|
+
- test/fixtures/public_key.pem
|
135
|
+
- test/fixtures/token.json
|
136
|
+
- test/initial_dev/test_data.md
|
137
|
+
- test/initial_dev/test_data_private_key.pem
|
138
|
+
- test/initial_dev/test_data_token.json
|
139
|
+
- test/payment_token_test.rb
|
140
|
+
- test/test_helper.rb
|