r2d2 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +8 -8
- data/Gemfile +0 -2
- data/README.md +78 -13
- data/lib/r2d2.rb +5 -1
- data/lib/r2d2/android_pay_token.rb +27 -0
- data/lib/r2d2/google_pay_token.rb +54 -0
- data/lib/r2d2/util.rb +104 -0
- data/lib/r2d2/version.rb +1 -1
- data/r2d2.gemspec +2 -1
- data/test/android_pay_token_test.rb +33 -0
- data/test/fixtures/ec_v1/card.json +5 -0
- data/test/fixtures/ec_v1/google_verification_key_production.json +8 -0
- data/test/fixtures/ec_v1/google_verification_key_test.json +8 -0
- data/test/fixtures/ec_v1/private_key.pem +5 -0
- data/test/fixtures/ec_v1/tokenized_card.json +5 -0
- data/test/google_pay_token_test.rb +105 -0
- data/test/test_helper.rb +1 -0
- data/test/token_builder_test.rb +37 -0
- data/test/util_test.rb +31 -0
- metadata +39 -7
- data/lib/r2d2/payment_token.rb +0 -82
- data/test/payment_token_test.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8fd5e6027086b7e2417c7839833ac9e5a4c895b42b8b798f59067dfc3974c82d
|
4
|
+
data.tar.gz: 40bcd0191eb22ea4f07b950fcc0628f287250dfb5476f128f0130463ecd91d34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96107bd836d140f4a5bcde53490faf3307ba7771fd50008e2f77d9ab61091163cad14872ff337579d48a8c41ee6d8a01b4f6fc2461fac725729db9f17365a9f0
|
7
|
+
data.tar.gz: f1762817389f164a1b5901497c46def4385646c66db5f945793bded47e459a99d60daf217bf9ee6c60a10d61433e35d1e2a8612c609325cffe2f5924dee1a898
|
data/.circleci/config.yml
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
version: 2
|
2
2
|
jobs:
|
3
|
-
ruby-2.1:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
10
|
ruby-2.2:
|
11
11
|
docker:
|
12
12
|
- image: circleci/ruby:2.2.10
|
@@ -39,7 +39,7 @@ workflows:
|
|
39
39
|
version: 2
|
40
40
|
rubies:
|
41
41
|
jobs:
|
42
|
-
- ruby-2.1
|
42
|
+
# - ruby-2.1
|
43
43
|
- ruby-2.2
|
44
44
|
# - ruby-2.3
|
45
45
|
# - ruby-2.4
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,21 +2,76 @@
|
|
2
2
|
|
3
3
|
[![CircleCI](https://circleci.com/gh/spreedly/r2d2.svg?style=svg)](https://circleci.com/gh/spreedly/r2d2)
|
4
4
|
|
5
|
-
R2D2 is a Ruby library for decrypting Android Pay payment tokens.
|
5
|
+
R2D2 is a Ruby library for decrypting Google Pay and Android Pay payment tokens.
|
6
6
|
|
7
7
|
## Ruby support
|
8
8
|
|
9
|
-
Currently,
|
9
|
+
Currently, Ruby v2.2 is 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
10
|
|
11
11
|
## Install
|
12
12
|
|
13
13
|
Add to your `Gemfile`:
|
14
14
|
|
15
15
|
```ruby
|
16
|
-
gem
|
16
|
+
gem 'r2d2', git: 'https://github.com/spreedly/r2d2.git'
|
17
17
|
```
|
18
18
|
|
19
|
-
## Usage
|
19
|
+
## Google Pay Usage
|
20
|
+
|
21
|
+
For Google Pay, R2D2 requires the token values in the form of a JSON hash, your `recipient_id`, Google's `verification_keys`,
|
22
|
+
and your private key.
|
23
|
+
|
24
|
+
Example Google Pay token values:
|
25
|
+
|
26
|
+
```json
|
27
|
+
{
|
28
|
+
"signature": "MEYCIQD5mAtwoptfXuDnEVvtSbPmRnkw94GXEHjog24SfIe4rAIhAKLeSY4xcHLK1liBoZFaeZG+FrqawI7Id2mJXwddP3KH",
|
29
|
+
"protocolVersion": "ECv1",
|
30
|
+
"signedMessage": "{\"encryptedMessage\":\"jzo38/Ufbt9qh/scrTJmG9v8Cgb7Y5S+zCTTbSou/NoLoE/XF9ixyIGNIspKkH4ulwwVX0/EoqKDKk86XDLw8qBjx1tfHefbLuhZbqkfu/8bs5D6QMz8LjcJU+EeXYcdZ+KeQ3jzrgS6B9CqEJJIF+PeySMJtTwF9Fh+X2sW4Yg0C34mHz0MHpVUpmzJZblTwzMkCVOdq7eMF9Ywb8kDnRFasMYALbRaEOMg2o9gXSfGEVPhS8ors4SRFcnLoVPfktHRJtY/UZEREJvGFY/s/wpmU9sRADYTMKQ/ChTMumT+1NG0r4XibDcaZjW/Wlz1Dwog+dNMYUblPjY613sBLtjoBbRDYYVuDn/TUYXOJwAgXoHFfMmvWm0ne0n9eXggxoaMFFgF5zXk9ZLl3FyH/hi3WWtsFt5sqQWgFdjsqTriL6i46m46hMaZ9gKZ8JQE912IG5kZts5L8XSMiG94Z3UiTA\\u003d\\u003d\",\"ephemeralPublicKey\":\"BIeq42AvLcEhz0oLmYdj++oBTS5PD131FAEgx4y91cwqbkZMUKADkzj2bD4MxneqgqFYirO29+y/G6YH9zmfjlk\\u003d\",\"tag\":\"sRILsawzbm53+9tVTh9ooBP5ivzxWki73UJbuOZ3IYY\\u003d\"}"
|
31
|
+
}
|
32
|
+
```
|
33
|
+
|
34
|
+
The `recipient_id` will be in the form `gateway:processorname`.
|
35
|
+
|
36
|
+
The `verificiation_keys` are available in Google's developer docs. Example:
|
37
|
+
|
38
|
+
```json
|
39
|
+
{ "keys":
|
40
|
+
[
|
41
|
+
{ "keyValue":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIsFro6K+IUxRr4yFTOTO+kFCCEvHo7B9IOMLxah6c977oFzX\/beObH4a9OfosMHmft3JJZ6B3xpjIb8kduK4\/A==",
|
42
|
+
"protocolVersion":"ECv1"
|
43
|
+
}
|
44
|
+
]
|
45
|
+
}
|
46
|
+
```
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'r2d2'
|
50
|
+
|
51
|
+
# token_attrs = Google Pay token values { "signature": "...", "protocolVersion": "...", ...}
|
52
|
+
token = R2D2.build_token(token_attrs, recipient_id: recipient_id, verification_keys: verification_keys)
|
53
|
+
|
54
|
+
private_key_pem = File.read('private_key.pem')
|
55
|
+
decrypted_json = token.decrypt(private_key_pem)
|
56
|
+
|
57
|
+
JSON.parse(decrypted_json)
|
58
|
+
# =>
|
59
|
+
{
|
60
|
+
"gatewayMerchantId" => "exampleGatewayMerchantId",
|
61
|
+
"messageExpiration" => "1528716120231",
|
62
|
+
"messageId" => "AH2EjtcpVGS3JvxlTP5kUbx3h0Laa30uVKjB9CqmnYiw8gZ-tpsxIoOdTbAU_DtCbkLVUPzkFeeqSbU1vTbAIAE4LlPHJqBiMMF4hZ5KRafml3764_6lK7aH7cQkIma40CI-rtCWTLCk",
|
63
|
+
"paymentMethod" => "CARD",
|
64
|
+
"paymentMethodDetails" =>
|
65
|
+
{
|
66
|
+
"expirationYear" => 2023,
|
67
|
+
"expirationMonth" => 12,
|
68
|
+
"pan" => "4111111111111111"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
|
74
|
+
## Android Pay Usage
|
20
75
|
|
21
76
|
R2D2 takes input in the form of the hash of Android Pay token values:
|
22
77
|
|
@@ -28,16 +83,15 @@ R2D2 takes input in the form of the hash of Android Pay token values:
|
|
28
83
|
}
|
29
84
|
```
|
30
85
|
|
31
|
-
and the merchant's private key
|
86
|
+
and the merchant's private key (which is managed by a third-party such as a gateway or independent processor like [Spreedly](https://spreedly.com)).
|
32
87
|
|
33
88
|
```ruby
|
34
|
-
require
|
89
|
+
require 'r2d2'
|
35
90
|
|
36
91
|
# token_json = raw token string you get from Android Pay { "encryptedMessage": "...", "tag": "...", ...}
|
37
|
-
|
38
|
-
token = R2D2::PaymentToken.new(token_attrs)
|
92
|
+
token = R2D2.build_token(token_attrs)
|
39
93
|
|
40
|
-
private_key_pem = File.read(
|
94
|
+
private_key_pem = File.read('private_key.pem')
|
41
95
|
decrypted_json = token.decrypt(private_key_pem)
|
42
96
|
|
43
97
|
JSON.parse(decrypted_json)
|
@@ -52,21 +106,21 @@ JSON.parse(decrypted_json)
|
|
52
106
|
}
|
53
107
|
```
|
54
108
|
|
55
|
-
|
109
|
+
## Performance
|
56
110
|
|
57
111
|
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
112
|
|
59
113
|
To enable `FastSecureCompare` in your environment, add the following to your Gemfile:
|
60
114
|
|
61
115
|
```ruby
|
62
|
-
gem 'fast_secure_compare
|
116
|
+
gem 'fast_secure_compare'
|
63
117
|
```
|
64
118
|
|
65
119
|
and require the extension in your application prior to loading r2d2:
|
66
120
|
|
67
121
|
```ruby
|
68
122
|
require 'fast_secure_compare/fast_secure_compare'
|
69
|
-
require 'r2d2
|
123
|
+
require 'r2d2'
|
70
124
|
```
|
71
125
|
|
72
126
|
Benchmarks illustrating the overhead of the pure Ruby version:
|
@@ -98,7 +152,7 @@ $ curl -u rwdaigle https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials
|
|
98
152
|
<enter rubygems account password>
|
99
153
|
```
|
100
154
|
|
101
|
-
If you are not yet listed as a gem owner, you will need to [request access](
|
155
|
+
If you are not yet listed as a gem owner, you will need to [request access](https://github.com/rwdaigle) from @rwdaigle.
|
102
156
|
|
103
157
|
### Release
|
104
158
|
|
@@ -110,6 +164,13 @@ $ rake release
|
|
110
164
|
|
111
165
|
## Changelog
|
112
166
|
|
167
|
+
### v1.0.0
|
168
|
+
|
169
|
+
* Breaking Changes: API now decrypts both Google Pay and Android Pay tokens
|
170
|
+
* New method call to decrypt Android Pay tokens
|
171
|
+
* Additional arguments included for Google Pay tokens
|
172
|
+
* Update README.md
|
173
|
+
|
113
174
|
### v0.1.2
|
114
175
|
|
115
176
|
* Setup CircleCI for more exhaustive Ruby version compatibility tests
|
@@ -117,4 +178,8 @@ $ rake release
|
|
117
178
|
|
118
179
|
## Contributors
|
119
180
|
|
181
|
+
* [mrezentes](https://github.com/mrezentes)
|
182
|
+
* [rwdaigle](https://github.com/rwdaigle)
|
120
183
|
* [methodmissing](https://github.com/methodmissing)
|
184
|
+
* [bdewater](https://github.com/bdewater)
|
185
|
+
* [deedeelavinder](https://github.com/deedeelavinder)
|
data/lib/r2d2.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module R2D2
|
2
|
+
class AndroidPayToken
|
3
|
+
include Util
|
4
|
+
|
5
|
+
attr_accessor :encrypted_message, :ephemeral_public_key, :tag
|
6
|
+
|
7
|
+
def initialize(token_attrs)
|
8
|
+
self.ephemeral_public_key = token_attrs["ephemeralPublicKey"]
|
9
|
+
self.tag = token_attrs["tag"]
|
10
|
+
self.encrypted_message = token_attrs["encryptedMessage"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def decrypt(private_key_pem)
|
14
|
+
private_key = OpenSSL::PKey::EC.new(private_key_pem)
|
15
|
+
|
16
|
+
shared_secret = generate_shared_secret(private_key, ephemeral_public_key)
|
17
|
+
|
18
|
+
# derive the symmetric_encryption_key and mac_key
|
19
|
+
hkdf_keys = derive_hkdf_keys(ephemeral_public_key, shared_secret, 'Android')
|
20
|
+
|
21
|
+
# verify the tag is a valid value
|
22
|
+
verify_mac(hkdf_keys[:mac_key], encrypted_message, tag)
|
23
|
+
|
24
|
+
JSON.parse(decrypt_message(encrypted_message, hkdf_keys[:symmetric_encryption_key]))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module R2D2
|
2
|
+
class GooglePayToken
|
3
|
+
include Util
|
4
|
+
|
5
|
+
attr_reader :protocol_version, :recipient_id, :verification_keys, :signature, :signed_message
|
6
|
+
|
7
|
+
def initialize(token_attrs, recipient_id:, verification_keys:)
|
8
|
+
@protocol_version = token_attrs['protocolVersion']
|
9
|
+
@recipient_id = recipient_id
|
10
|
+
@verification_keys = verification_keys
|
11
|
+
@signature = token_attrs['signature']
|
12
|
+
@signed_message = token_attrs['signedMessage']
|
13
|
+
end
|
14
|
+
|
15
|
+
def decrypt(private_key_pem)
|
16
|
+
verified = verify_and_parse_message
|
17
|
+
|
18
|
+
private_key = OpenSSL::PKey::EC.new(private_key_pem)
|
19
|
+
shared_secret = generate_shared_secret(private_key, verified['ephemeralPublicKey'])
|
20
|
+
hkdf_keys = derive_hkdf_keys(verified['ephemeralPublicKey'], shared_secret, 'Google')
|
21
|
+
|
22
|
+
verify_mac(hkdf_keys[:mac_key], verified['encryptedMessage'], verified['tag'])
|
23
|
+
decrypted = JSON.parse(
|
24
|
+
decrypt_message(verified['encryptedMessage'], hkdf_keys[:symmetric_encryption_key])
|
25
|
+
)
|
26
|
+
|
27
|
+
expired = decrypted['messageExpiration'].to_f / 1000.0 <= Time.now.to_f
|
28
|
+
raise MessageExpiredError if expired
|
29
|
+
|
30
|
+
decrypted
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def verify_and_parse_message
|
36
|
+
digest = OpenSSL::Digest::SHA256.new
|
37
|
+
signed_bytes = to_length_value(
|
38
|
+
'Google',
|
39
|
+
recipient_id,
|
40
|
+
protocol_version,
|
41
|
+
signed_message
|
42
|
+
)
|
43
|
+
verified = verification_keys['keys'].any? do |key|
|
44
|
+
next if key['protocolVersion'] != protocol_version
|
45
|
+
|
46
|
+
ec = OpenSSL::PKey::EC.new(Base64.strict_decode64(key['keyValue']))
|
47
|
+
ec.verify(digest, Base64.strict_decode64(signature), signed_bytes)
|
48
|
+
end
|
49
|
+
|
50
|
+
raise SignatureInvalidError unless verified
|
51
|
+
JSON.parse(signed_message)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/r2d2/util.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
module R2D2
|
2
|
+
Error = Class.new(StandardError)
|
3
|
+
TagVerificationError = Class.new(R2D2::Error)
|
4
|
+
SignatureInvalidError = Class.new(R2D2::Error)
|
5
|
+
MessageExpiredError = Class.new(R2D2::Error)
|
6
|
+
|
7
|
+
def build_token(token_attrs, recipient_id: nil, verification_keys: nil)
|
8
|
+
protocol_version = token_attrs.fetch('protocolVersion', 'ECv0')
|
9
|
+
|
10
|
+
case protocol_version
|
11
|
+
when 'ECv0'
|
12
|
+
AndroidPayToken.new(token_attrs)
|
13
|
+
when 'ECv1'
|
14
|
+
raise ArgumentError, "missing keyword: recipient_id" if recipient_id.nil?
|
15
|
+
raise ArgumentError, "missing keyword: verification_keys" if verification_keys.nil?
|
16
|
+
|
17
|
+
GooglePayToken.new(token_attrs, recipient_id: recipient_id, verification_keys: verification_keys)
|
18
|
+
else
|
19
|
+
raise ArgumentError, "unknown protocolVersion #{protocol_version}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
module_function :build_token
|
23
|
+
|
24
|
+
module Util
|
25
|
+
def generate_shared_secret(private_key, ephemeral_public_key)
|
26
|
+
ec = OpenSSL::PKey::EC.new('prime256v1')
|
27
|
+
bn = OpenSSL::BN.new(Base64.decode64(ephemeral_public_key), 2)
|
28
|
+
point = OpenSSL::PKey::EC::Point.new(ec.group, bn)
|
29
|
+
private_key.dh_compute_key(point)
|
30
|
+
end
|
31
|
+
|
32
|
+
def derive_hkdf_keys(ephemeral_public_key, shared_secret, info)
|
33
|
+
key_material = Base64.decode64(ephemeral_public_key) + shared_secret
|
34
|
+
hkdf_bytes = hkdf(key_material, info)
|
35
|
+
{
|
36
|
+
symmetric_encryption_key: hkdf_bytes[0..15],
|
37
|
+
mac_key: hkdf_bytes[16..32]
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def verify_mac(mac_key, encrypted_message, tag)
|
42
|
+
digest = OpenSSL::Digest.new('sha256')
|
43
|
+
mac = OpenSSL::HMAC.digest(digest, mac_key, Base64.decode64(encrypted_message))
|
44
|
+
raise TagVerificationError unless secure_compare(mac, Base64.decode64(tag))
|
45
|
+
end
|
46
|
+
|
47
|
+
def decrypt_message(encrypted_data, symmetric_key)
|
48
|
+
decipher = OpenSSL::Cipher::AES128.new(:CTR)
|
49
|
+
decipher.decrypt
|
50
|
+
decipher.key = symmetric_key
|
51
|
+
decipher.update(Base64.decode64(encrypted_data)) + decipher.final
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_length_value(*chunks)
|
55
|
+
value = ''
|
56
|
+
chunks.each do |chunk|
|
57
|
+
chunk_size = 4.times.map do |index|
|
58
|
+
(chunk.bytesize >> (8 * index)) & 0xFF
|
59
|
+
end
|
60
|
+
value << chunk_size.pack('C*')
|
61
|
+
value << chunk
|
62
|
+
end
|
63
|
+
value
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
if defined?(FastSecureCompare)
|
69
|
+
def secure_compare(a, b)
|
70
|
+
FastSecureCompare.compare(a, b)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
# constant-time comparison algorithm to prevent timing attacks; borrowed from ActiveSupport::MessageVerifier
|
74
|
+
def secure_compare(a, b)
|
75
|
+
return false unless a.bytesize == b.bytesize
|
76
|
+
|
77
|
+
l = a.unpack("C#{a.bytesize}")
|
78
|
+
|
79
|
+
res = 0
|
80
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
81
|
+
res == 0
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
if defined?(OpenSSL::KDF) && OpenSSL::KDF.respond_to?(:hkdf)
|
86
|
+
def hkdf(key_material, info)
|
87
|
+
OpenSSL::KDF.hkdf(key_material, salt: 0.chr * 32, info: info, length: 32, hash: 'sha256')
|
88
|
+
end
|
89
|
+
else
|
90
|
+
begin
|
91
|
+
require 'hkdf'
|
92
|
+
rescue LoadError
|
93
|
+
STDERR.puts "You need at least Ruby OpenSSL gem 2.1 (installed: #{OpenSSL::VERSION}) " \
|
94
|
+
"and system OpenSSL 1.1.0 (installed: #{OpenSSL::OPENSSL_LIBRARY_VERSION}) for HKDF support." \
|
95
|
+
"You can add \"gem 'hkdf'\" to your Gemfile for a Ruby-based fallback."
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
|
99
|
+
def hkdf(key_material, info)
|
100
|
+
HKDF.new(key_material, algorithm: 'SHA256', info: info).next_bytes(32)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/r2d2/version.rb
CHANGED
data/r2d2.gemspec
CHANGED
@@ -16,12 +16,13 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
-
s.required_ruby_version = ">=
|
19
|
+
s.required_ruby_version = ">= 2.2"
|
20
20
|
|
21
21
|
s.add_runtime_dependency 'hkdf'
|
22
22
|
|
23
23
|
s.add_development_dependency "bundler", "~> 1.15"
|
24
24
|
s.add_development_dependency "rake", "~> 12.0"
|
25
25
|
s.add_development_dependency "minitest", "~> 5.0"
|
26
|
+
s.add_development_dependency "timecop"
|
26
27
|
s.add_development_dependency "pry-byebug"
|
27
28
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class R2D2::AndroidPayTokenTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
fixtures = File.dirname(__FILE__) + "/fixtures/"
|
6
|
+
@token_attrs = JSON.parse(File.read(fixtures + "token.json"))
|
7
|
+
@private_key = File.read(fixtures + "private_key.pem")
|
8
|
+
@payment_token = R2D2::AndroidPayToken.new(@token_attrs)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_initialize
|
12
|
+
assert_equal @token_attrs["ephemeralPublicKey"], @payment_token.ephemeral_public_key
|
13
|
+
assert_equal @token_attrs["tag"], @payment_token.tag
|
14
|
+
assert_equal @token_attrs["encryptedMessage"], @payment_token.encrypted_message
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_successful_decrypt
|
18
|
+
payment_data = @payment_token.decrypt(@private_key)
|
19
|
+
assert_equal "4895370012003478", payment_data["dpan"]
|
20
|
+
assert_equal 12, payment_data["expirationMonth"]
|
21
|
+
assert_equal 2020, payment_data["expirationYear"]
|
22
|
+
assert_equal "3DS", payment_data["authMethod"]
|
23
|
+
assert_equal "AgAAAAAABk4DWZ4C28yUQAAAAAA=", payment_data["3dsCryptogram"]
|
24
|
+
assert_equal "07", payment_data["3dsEciIndicator"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_invalid_tag
|
28
|
+
@payment_token.tag = "SomethingBogus"
|
29
|
+
assert_raises R2D2::TagVerificationError do
|
30
|
+
@payment_token.decrypt(@private_key)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
{
|
2
|
+
"signature": "MEUCIQDeIvCbFje6fFQe0lFUD8oV6T3GY81dU9UJ6KeXlwKSJgIgamgC56BdcNEJgffQDW3bTFWmEQVoC6zUZ+J7xJp2WAg\u003d",
|
3
|
+
"protocolVersion": "ECv1",
|
4
|
+
"signedMessage": "{\"encryptedMessage\":\"ilnxpVEEaaZ2GFSc7cQDNxSMkjF5nWFcT6w3HSdpX+5mXn/uv6naeTjSoGfy1gi6JdpToKuc0hhkqpetNVDj6dPH8u01K685xEL0AVjuSR84WnRfxnWjDNlYRuGS0N5iPY6G/981ucbgwBNcAjqemTdc6xv6/lQwZq36V7FWt4IAchg55JJt2mnFMSDkkMTz5bWfJtgmwg55b0KXfuTA3ADLxeFlOLuT3DICFzo45jyCK1RSltiCHhD+Lm65wESCHCgS9W50yh2a/J1lTCUP7Xy/aaurVtNUrB+H/SlY16Szk455h27d2Zi6JKzxnL/rQ+5ME1TWpp0n66RQ70y1roC5myNy6ILmEoyIdAU6FahuGX4MQFkmO4G4GOL5lMEj63hF1U2N/iFm03Urx2dSWkCsiVM\\u003d\",\"ephemeralPublicKey\":\"BEENTno093zpl+QST4mXKVqrOVHsqweffqILO+HAob2JyF8YNtjBWXoKunSF0rkGY3spzp/BsNHKJ3Req+DfOwM\\u003d\",\"tag\":\"eZJ0p0DUu9TGM3bOx8VBCcgt9sJ9tgLRnLjgPyd4aKs\\u003d\"}"
|
5
|
+
}
|
@@ -0,0 +1,5 @@
|
|
1
|
+
{
|
2
|
+
"signature": "MEUCIQDzgQkY/Lj42FoZC9wN44Uz0r6uDttc68k2c9oZCzNJPwIgSRukEx30uMjqzFswekoarAtO9p1/E221hNmVuHzDW+M\u003d",
|
3
|
+
"protocolVersion": "ECv1",
|
4
|
+
"signedMessage": "{\"encryptedMessage\":\"RrliMMApO0MghHfjfVbJDz3jmdwQYluu8yjLPqnsRUZ36s28ceb+KxEdFIaxC2Y/d6hUm7kUOpyLbCMlj0IZXUP7WmGxHW+oQr8BDiHmwlRzovO3WCyB91kci0dz17k5qKaT/6Pf3FMu+0IhiGd+QtD6d+ifv6bvy6iA0rwpGMHheC/gKqZF4xCYWJjw7Twa72xEbjp9qlO0msA8jysjuB+N2injsL43I/lB592I/V9VZJvI5S7TDZi8XQb1kNKNmhH1XDEseT3Mb180xtJxCxuVOpk9yH7NyRWha3HV6Tgr+GqSSk5jCzZyY/PxxwTjnRvRPmcrd7PTN+w1k4M4TdniyStmdk8yUdxgd+utNB3Xwh4J8caFtDVZdRYgoDx5S/3zJ6xQckqQzVD0fRuykVtMjY++EMbNfNdGKV0u8mAEBVXJaPVsxISCgBJTvpNzllj3DMvNEE/Zc5p+7izMh8mz++g5ab5mrw/Xg//dYm/ndKG6V9WXqosTg4IdhsWALgCLyYVZwxjsjF3kua0XZ2IyemsvGU0KtZL9Uy4\\u003d\",\"ephemeralPublicKey\":\"BCVPKilVgN/Mg7cg4nkLZnvMZ0EIONH+Cnlq6ykhWk/lq+fHHgH+lOoy0MXvI7ZfDL6AesSO8+mqLl4MwwHDd6o\\u003d\",\"tag\":\"P/m+wFoQ7yQHepwhGdGvp08TSPPuO1a5SyF9C5QKIe4\\u003d\"}"
|
5
|
+
}
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module R2D2
|
4
|
+
class GooglePayTokenTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@recipient_id = 'merchant:12345678901234567890'
|
7
|
+
@fixtures = __dir__ + "/fixtures/ec_v1/"
|
8
|
+
@token = JSON.parse(File.read(@fixtures + "tokenized_card.json"))
|
9
|
+
@private_key = File.read(@fixtures + "private_key.pem")
|
10
|
+
@verification_keys = JSON.parse(File.read(@fixtures + "google_verification_key_test.json"))
|
11
|
+
Timecop.freeze(Time.at(1509713963))
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
Timecop.return
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_decrypted_tokenized_card
|
19
|
+
expected = {
|
20
|
+
"messageExpiration" => "1510318759535",
|
21
|
+
"paymentMethod" => "TOKENIZED_CARD",
|
22
|
+
"messageId" => "AH2EjtfMnpeHvgqYbBDLAxPzyYlPOmOa792BqdsvTc2T7jsn23_us0dKU509I-AA9dVDLf9_v4c5ldxoge6Q3iYr9acGGSyD9ojbOTP1fjWzDteVE_yf1pGzGNQ2Q6jKG96KRpbIaziY",
|
23
|
+
"paymentMethodDetails" =>
|
24
|
+
{
|
25
|
+
"expirationYear" => 2022,
|
26
|
+
"dpan" => "4895370012003478",
|
27
|
+
"expirationMonth" => 12,
|
28
|
+
"authMethod" => "3DS",
|
29
|
+
"3dsCryptogram" => "AgAAAAAABk4DWZ4C28yUQAAAAAA=",
|
30
|
+
"3dsEciIndicator" => "07"
|
31
|
+
}
|
32
|
+
}
|
33
|
+
decrypted = new_token.decrypt(@private_key)
|
34
|
+
|
35
|
+
assert_equal expected, decrypted
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_decrypted_card
|
39
|
+
@token = JSON.parse(File.read(@fixtures + 'card.json'))
|
40
|
+
expected = {
|
41
|
+
"messageExpiration" => "1510319499834",
|
42
|
+
"paymentMethod" => "CARD",
|
43
|
+
"messageId" => "AH2EjtcMeg5mOCD9kUXWn6quP6AF6jOJeirO0EW40tPVzMMy_YAri8HZdJzqzQquC0w_dkvXhC41s2BN53HRD_kzgT4jxGeB4E9BI8OQCPw9GgWTXIAQb55Av77l6VCesYHIQre8Ij60",
|
44
|
+
"paymentMethodDetails" =>
|
45
|
+
{
|
46
|
+
"expirationYear" => 2022,
|
47
|
+
"expirationMonth" => 12,
|
48
|
+
"pan" => "4111111111111111"
|
49
|
+
}
|
50
|
+
}
|
51
|
+
decrypted = new_token.decrypt(@private_key)
|
52
|
+
|
53
|
+
assert_equal expected, decrypted
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_wrong_signature
|
57
|
+
@token['signature'] = "MEQCIDxBoUCoFRGReLdZ/cABlSSRIKoOEFoU3e27c14vMZtfAiBtX3pGMEpnw6mSAbnagCCgHlCk3NcFwWYEyxIE6KGZVA\u003d\u003d"
|
58
|
+
|
59
|
+
assert_raises R2D2::SignatureInvalidError do
|
60
|
+
new_token.decrypt(@private_key)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_wrong_verification_key
|
65
|
+
@verification_keys = JSON.parse(File.read(@fixtures + "google_verification_key_production.json"))
|
66
|
+
|
67
|
+
assert_raises R2D2::SignatureInvalidError do
|
68
|
+
new_token.decrypt(@private_key)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_unknown_verification_key_version
|
73
|
+
@verification_keys['keys'][0]['protocolVersion'] = 'foo'
|
74
|
+
|
75
|
+
assert_raises R2D2::SignatureInvalidError do
|
76
|
+
new_token.decrypt(@private_key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_multiple_verification_keys
|
81
|
+
production_keys = JSON.parse(File.read(@fixtures + "google_verification_key_production.json"))['keys']
|
82
|
+
@verification_keys = { 'keys' => production_keys + @verification_keys['keys'] }
|
83
|
+
|
84
|
+
assert new_token.decrypt(@private_key)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_expired_message
|
88
|
+
Timecop.freeze(Time.at(1510318760)) do
|
89
|
+
assert_raises R2D2::MessageExpiredError do
|
90
|
+
new_token.decrypt(@private_key)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def new_token
|
98
|
+
R2D2::GooglePayToken.new(
|
99
|
+
@token,
|
100
|
+
recipient_id: @recipient_id,
|
101
|
+
verification_keys: @verification_keys
|
102
|
+
)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module R2D2
|
4
|
+
class TokenBuilderTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@fixtures = __dir__ + "/fixtures/"
|
7
|
+
@recipient_id = 'merchant:12345678901234567890'
|
8
|
+
@verification_keys = JSON.parse(File.read(@fixtures + "ec_v1/google_verification_key_test.json"))
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_builds_android_pay_token
|
12
|
+
token_attrs = JSON.parse(File.read(@fixtures + "token.json"))
|
13
|
+
assert_instance_of AndroidPayToken, R2D2.build_token(token_attrs)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_builds_google_pay_token
|
17
|
+
token_attrs = JSON.parse(File.read(@fixtures + "ec_v1/tokenized_card.json"))
|
18
|
+
assert_instance_of GooglePayToken, R2D2.build_token(token_attrs, recipient_id: @recipient_id, verification_keys: @verification_keys)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_building_token_raises_with_unknown_protocol_version
|
22
|
+
token_attrs = JSON.parse(File.read(@fixtures + "ec_v1/tokenized_card.json"))
|
23
|
+
token_attrs['protocolVersion'] = 'foo'
|
24
|
+
|
25
|
+
assert_raises ArgumentError do
|
26
|
+
R2D2.build_token(token_attrs, recipient_id: @recipient_id, verification_keys: @verification_keys)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_building_google_pay_token_raises_with_missing_arguments
|
31
|
+
token_attrs = JSON.parse(File.read(@fixtures + "ec_v1/tokenized_card.json"))
|
32
|
+
assert_raises ArgumentError do
|
33
|
+
R2D2.build_token(token_attrs)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/test/util_test.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module R2D2
|
4
|
+
class UtilTest < Minitest::Test
|
5
|
+
include Util
|
6
|
+
|
7
|
+
def setup
|
8
|
+
fixtures = __dir__ + "/fixtures/"
|
9
|
+
@token_attrs = JSON.parse(File.read(fixtures + "token.json"))
|
10
|
+
@private_key = File.read(fixtures + "private_key.pem")
|
11
|
+
@payment_token = R2D2::AndroidPayToken.new(@token_attrs)
|
12
|
+
@shared_secret = ['44a9715c18ebcb255af705f7332657420aca40604334a7d48a89baba18280a97']
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_shared_secret
|
16
|
+
priv_key = OpenSSL::PKey::EC.new(@private_key)
|
17
|
+
assert_equal @shared_secret, generate_shared_secret(priv_key, @payment_token.ephemeral_public_key).unpack('H*')
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_derive_hkdf_keys
|
21
|
+
hkdf_keys = derive_hkdf_keys(@payment_token.ephemeral_public_key, @shared_secret[0], 'Android')
|
22
|
+
assert_equal ["c7b2670dc0630edd0a9101dd5d70e4b2"], hkdf_keys[:symmetric_encryption_key].unpack('H*')
|
23
|
+
assert_equal ["d8976b95c980760d8ce3933994c6eda1"], hkdf_keys[:mac_key].unpack('H*')
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_to_length_value
|
27
|
+
expected = "\x06\x00\x00\x00Google\x04\x00\x00\x00ECv1\x13\x01\x00\x00" + 'longstring-' * 25
|
28
|
+
assert_equal expected, to_length_value('Google', 'ECv1', 'longstring-' * 25)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: r2d2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miki Rezentes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-
|
12
|
+
date: 2018-06-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hkdf
|
@@ -67,6 +67,20 @@ dependencies:
|
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: '5.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: timecop
|
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'
|
70
84
|
- !ruby/object:Gem::Dependency
|
71
85
|
name: pry-byebug
|
72
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,17 +109,27 @@ files:
|
|
95
109
|
- README.md
|
96
110
|
- Rakefile
|
97
111
|
- lib/r2d2.rb
|
98
|
-
- lib/r2d2/
|
112
|
+
- lib/r2d2/android_pay_token.rb
|
113
|
+
- lib/r2d2/google_pay_token.rb
|
114
|
+
- lib/r2d2/util.rb
|
99
115
|
- lib/r2d2/version.rb
|
100
116
|
- r2d2.gemspec
|
117
|
+
- test/android_pay_token_test.rb
|
118
|
+
- test/fixtures/ec_v1/card.json
|
119
|
+
- test/fixtures/ec_v1/google_verification_key_production.json
|
120
|
+
- test/fixtures/ec_v1/google_verification_key_test.json
|
121
|
+
- test/fixtures/ec_v1/private_key.pem
|
122
|
+
- test/fixtures/ec_v1/tokenized_card.json
|
101
123
|
- test/fixtures/private_key.pem
|
102
124
|
- test/fixtures/public_key.pem
|
103
125
|
- test/fixtures/token.json
|
126
|
+
- test/google_pay_token_test.rb
|
104
127
|
- test/initial_dev/test_data.md
|
105
128
|
- test/initial_dev/test_data_private_key.pem
|
106
129
|
- test/initial_dev/test_data_token.json
|
107
|
-
- test/payment_token_test.rb
|
108
130
|
- test/test_helper.rb
|
131
|
+
- test/token_builder_test.rb
|
132
|
+
- test/util_test.rb
|
109
133
|
homepage: https://github.com/spreedly/r2d2
|
110
134
|
licenses: []
|
111
135
|
metadata: {}
|
@@ -117,7 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
141
|
requirements:
|
118
142
|
- - ">="
|
119
143
|
- !ruby/object:Gem::Version
|
120
|
-
version:
|
144
|
+
version: '2.2'
|
121
145
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
146
|
requirements:
|
123
147
|
- - ">="
|
@@ -125,16 +149,24 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
149
|
version: '0'
|
126
150
|
requirements: []
|
127
151
|
rubyforge_project:
|
128
|
-
rubygems_version: 2.
|
152
|
+
rubygems_version: 2.7.5
|
129
153
|
signing_key:
|
130
154
|
specification_version: 4
|
131
155
|
summary: Android Pay payment token decryption library
|
132
156
|
test_files:
|
157
|
+
- test/android_pay_token_test.rb
|
158
|
+
- test/fixtures/ec_v1/card.json
|
159
|
+
- test/fixtures/ec_v1/google_verification_key_production.json
|
160
|
+
- test/fixtures/ec_v1/google_verification_key_test.json
|
161
|
+
- test/fixtures/ec_v1/private_key.pem
|
162
|
+
- test/fixtures/ec_v1/tokenized_card.json
|
133
163
|
- test/fixtures/private_key.pem
|
134
164
|
- test/fixtures/public_key.pem
|
135
165
|
- test/fixtures/token.json
|
166
|
+
- test/google_pay_token_test.rb
|
136
167
|
- test/initial_dev/test_data.md
|
137
168
|
- test/initial_dev/test_data_private_key.pem
|
138
169
|
- test/initial_dev/test_data_token.json
|
139
|
-
- test/payment_token_test.rb
|
140
170
|
- test/test_helper.rb
|
171
|
+
- test/token_builder_test.rb
|
172
|
+
- test/util_test.rb
|
data/lib/r2d2/payment_token.rb
DELETED
@@ -1,82 +0,0 @@
|
|
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/test/payment_token_test.rb
DELETED
@@ -1,49 +0,0 @@
|
|
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
|