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