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 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