r2d2 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ vendor/*
2
+ .bundle
3
+ notes.md
4
+ Gemfile.lock
5
+ .ruby-version
6
+ .ruby-gemset
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'hkdf', '~> 0.2.0'
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,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,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
@@ -0,0 +1,3 @@
1
+ module R2D2
2
+ VERSION = "0.1.2" unless defined? R2D2::VERSION
3
+ end
data/lib/r2d2.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "json"
2
+
3
+ require_relative "r2d2/payment_token"
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
+ -----BEGIN EC PRIVATE KEY-----
2
+ MHcCAQEEIDnEBl2fHeMqFqePupLh6RTQM6Ro16v8JjIAVXcHp4ktoAoGCCqGSM49
3
+ AwEHoUQDQgAEa6fxL04JEhOi/+1QzTHuh6d+qoEizAo79xNkJ5xvaeizZv2wBRV+
4
+ cynhOeThDf8FJDE4TIGL0G+a4zlrM3wqNw==
5
+ -----END EC PRIVATE KEY-----
@@ -0,0 +1,4 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa6fxL04JEhOi/+1QzTHuh6d+qoEi
3
+ zAo79xNkJ5xvaeizZv2wBRV+cynhOeThDf8FJDE4TIGL0G+a4zlrM3wqNw==
4
+ -----END PUBLIC KEY-----
@@ -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 P­126 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
+ * Fixed­timing 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 merchant­key.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,5 @@
1
+ -----BEGIN EC PRIVATE KEY-----
2
+ MHcCAQEEIAj0rha+IkiGkKa443IRz8g7tjVxXtLwwaE6T5GGivXXoAoGCCqGSM49
3
+ AwEHoUQDQgAE52hc/70CrjvdKcbCDclTVqI2mx328faiCerg+0O2UsZrcqPxM9/6
4
+ NpA0/INKSHclSGJLQrKuuVaECA1kodgXZg==
5
+ -----END EC PRIVATE KEY-----
@@ -0,0 +1,5 @@
1
+ {
2
+ "ephemeralPublicKey":"BPhVspn70Zj2Kkgu9t8+ApEuUWsI/zos5whGCQBlgOkuYagOis7qsrcbQrcprjvTZO3XOU+Qbcc28FSgsRtcgQE=",
3
+ "encryptedMessage":"PHxZxBQvVWwP",
4
+ "tag":"s9wa3Q2WiyGi/eDA4XYVklq08KZiSxB7xvRiKK3H7kE="
5
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ require "r2d2"
3
+
4
+ require "minitest/autorun"
5
+ require "pry-byebug"
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