r2d2 0.1.2
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.
- 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
|
+
[](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
|