aliquot-pay 0.10.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aliquot-pay.rb +178 -78
  3. data/lib/aliquot-pay/util.rb +15 -6
  4. metadata +21 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f10cb5f87859a89e6be2cd9c7b44755d055d53316e4a306a9efc5cff4b973cc
4
- data.tar.gz: '043648edcd44e8984ace19066bcca9941d70ac8c79bc7c148cc92cc4fb8b6758'
3
+ metadata.gz: 3b09f20a26a4ba4bd85ef274044bca8f84fd5471c4393b7e14e3aae8a8ee9e48
4
+ data.tar.gz: aa5e97d77be4ce999063378eb5bce12f4bbd6e7509fbd3f100388281197175cd
5
5
  SHA512:
6
- metadata.gz: 3f7ccb1a31f5138835613f43b56f0ebd740c257496ff435c35c250c23acc860628326656b7159522867d08e574105fc6cc58c1c6ff75cfbbe2a5d85b3efff77e
7
- data.tar.gz: 0e107069afffbef7ef490bf2f48863f13ec0c6afdd425e909d2a4318f4d96dff0b90aaadd755a3daa627654724b4bcb7be024b6dc511c08f7a81152dc77e832c
6
+ metadata.gz: 1b0747731b0320552cc308b76a32c2786358a9ddd74136498da0b3145981f9584ae184d06fe903cede3b815fc6dc5f82fbccf24dde754aebb3a35961d5df4db9
7
+ data.tar.gz: 71018fd9c9885e435a298bd8138f471494bba598a0608c7e52f73d85625b597d4f2993df82cae1c4448dfa6bdd14682864d10692e855433cc14010d7cf032a5e
@@ -4,27 +4,79 @@ require 'json'
4
4
 
5
5
  require 'aliquot-pay/util'
6
6
 
7
- module AliquotPay
7
+ class AliquotPay
8
8
  class Error < StandardError; end
9
9
 
10
10
  EC_CURVE = 'prime256v1'.freeze
11
11
 
12
12
  DEFAULTS = {
13
13
  info: 'Google',
14
- merchant_id: '0123456789',
14
+ recipient_id: 'merchant:0123456789',
15
15
  }.freeze
16
16
 
17
- def self.sign(key, message)
17
+ attr_accessor :signature, :intermediate_signing_key, :signed_message
18
+ attr_accessor :signed_key, :signatures
19
+ attr_accessor :key_expiration, :key_value
20
+ attr_accessor :encrypted_message, :cleartext_message, :ephemeral_public_key, :tag
21
+ attr_accessor :message_expiration, :message_id, :payment_method, :payment_method_details
22
+ attr_accessor :pan, :expiration_month, :expiration_year, :auth_method
23
+ attr_accessor :cryptogram, :eci_indicator
24
+
25
+ attr_accessor :recipient, :info, :root_key, :intermediate_key
26
+ attr_writer :recipient_id, :shared_secret, :token, :signed_key_string
27
+
28
+ def initialize(protocol_version = :ECv2)
29
+ @protocol_version = protocol_version
30
+ end
31
+
32
+ def token
33
+ build_token
34
+ end
35
+
36
+ def extract_root_signing_keys
37
+ key = Base64.strict_encode64(eckey_to_public(ensure_root_key).to_der)
38
+ {
39
+ 'keys' => [
40
+ 'protocolVersion' => @protocol_version,
41
+ 'keyValue' => key,
42
+ ]
43
+ }.to_json
44
+ end
45
+
46
+ def eckey_to_public(key)
47
+ p = OpenSSL::PKey::EC.new(EC_CURVE)
48
+
49
+ p.public_key = key.public_key
50
+
51
+ p
52
+ end
53
+
54
+ #private
55
+
56
+ def sign(key, message)
18
57
  d = OpenSSL::Digest::SHA256.new
19
58
  def key.private?; private_key?; end
20
59
  Base64.strict_encode64(key.sign(d, message))
21
60
  end
22
61
 
23
- def self.encrypt(cleartext_message, recipient, cipher, info = 'Google')
62
+ def encrypt(cleartext_message)
63
+ @recipient ||= OpenSSL::PKey::EC.new('prime256v1').generate_key
64
+ @info ||= 'Google'
65
+
24
66
  eph = AliquotPay::Util.generate_ephemeral_key
25
- ss = AliquotPay::Util.generate_shared_secret(eph, recipient.public_key)
67
+ @shared_secret ||= AliquotPay::Util.generate_shared_secret(eph, @recipient.public_key)
68
+ ss = @shared_secret
69
+
70
+ case @protocol_version
71
+ when :ECv1
72
+ cipher = OpenSSL::Cipher::AES128.new(:CTR)
73
+ when :ECv2
74
+ cipher = OpenSSL::Cipher::AES256.new(:CTR)
75
+ else
76
+ raise StandardError, "Invalid protocol_version #{protocol_version}"
77
+ end
26
78
 
27
- keys = AliquotPay::Util.derive_keys(eph.public_key.to_bn.to_s(2), ss, info, length: cipher.key_len)
79
+ keys = AliquotPay::Util.derive_keys(eph.public_key.to_bn.to_s(2), ss, @info, @protocol_version)
28
80
 
29
81
  cipher.encrypt
30
82
  cipher.key = keys[:aes_key]
@@ -34,101 +86,149 @@ module AliquotPay
34
86
  tag = AliquotPay::Util.calculate_tag(keys[:mac_key], encrypted_message)
35
87
 
36
88
  {
37
- encryptedMessage: Base64.strict_encode64(encrypted_message),
38
- ephemeralPublicKey: Base64.strict_encode64(eph.public_key.to_bn.to_s(2)),
39
- tag: Base64.strict_encode64(tag),
89
+ 'encryptedMessage' => Base64.strict_encode64(encrypted_message),
90
+ 'ephemeralPublicKey' => Base64.strict_encode64(eph.public_key.to_bn.to_s(2)),
91
+ 'tag' => Base64.strict_encode64(tag),
40
92
  }
41
93
  end
42
94
 
43
- # Return a default payment
44
- def self.payment(
45
- auth_method: :PAN_ONLY,
46
- expiration: ((Time.now.to_f + 60 * 5) * 1000).round.to_s
47
- )
48
- id = Base64.strict_encode64(OpenSSL::Random.random_bytes(24))
49
- p = {
50
- 'messageExpiration' => expiration,
51
- 'messageId' => id,
52
- 'paymentMethod' => 'CARD',
53
- 'paymentMethodDetails' => {
54
- 'expirationYear' => 2023,
55
- 'expirationMonth' => 12,
56
- 'pan' => '4111111111111111',
57
- 'authMethod' => 'PAN_ONLY',
58
- },
95
+ def build_payment_method_details
96
+ return @payment_method_details if @payment_method_details
97
+ value = {
98
+ 'pan' => @pan || '4111111111111111',
99
+ 'expirationYear' => @expiration_year || 2023,
100
+ 'expirationMonth' => @expiration_month || 12,
101
+ 'authMethod' => @auth_method || 'PAN_ONLY',
59
102
  }
60
103
 
61
- if auth_method == :CRYPTOGRAM_3DS
62
- p['paymentMethodDetails']['authMethod'] = 'CRYPTOGRAM_3DS'
63
- p['paymentMethodDetails']['cryptogram'] = 'SOME CRYPTOGRAM'
64
- p['paymentMethodDetails']['eciIndicator'] = '05'
104
+ if @auth_method == 'CRYPTOGRAM_3DS'
105
+ value.merge!(
106
+ 'cryptogram' => @cryptogram || 'SOME CRYPTOGRAM',
107
+ 'eciIndicator' => @eci_indicator || '05'
108
+ )
65
109
  end
66
110
 
67
- p
111
+ value
68
112
  end
69
113
 
70
- # Return a string length as a 4byte little-endian integer, as a string
71
- def self.four_byte_length(str)
72
- [str.length].pack('V')
73
- end
114
+ def build_cleartext_message
115
+ return @cleartext_message if @cleartext_message
116
+ default_message_id = Base64.strict_encode64(OpenSSL::Random.random_bytes(24))
117
+ default_message_expiration = ((Time.now.to_f + 60 * 5) * 1000).round.to_s
74
118
 
75
- def self.generate_signature(*args)
76
- args.map do |s|
77
- four_byte_length(s) + s
78
- end.join('')
119
+ @cleartext_message = {
120
+ 'messageExpiration' => @message_expiration || default_message_expiration,
121
+ 'messageId' => @message_id || default_message_id,
122
+ 'paymentMethod' => @payment_method || 'CARD',
123
+ 'paymentMethodDetails' => build_payment_method_details
124
+ }
79
125
  end
80
126
 
81
- def self.signature_string(
82
- message,
83
- recipient_id: DEFAULTS[:merchant_id],
84
- sender_id: DEFAULTS[:info],
85
- protocol_version: 'ECv1'
86
- )
127
+ def build_signed_message
128
+ return @signed_message if @signed_message
129
+
130
+ signed_message = encrypt(build_cleartext_message.to_json)
131
+ signed_message['encryptedMessage'] = @encrypted_message if @encrypted_message
132
+ signed_message['ephemeralPublicKey'] = @ephemeral_public_key if @ephemeral_public_key
133
+ signed_message['tag'] = @tag if @tag
134
+
135
+ @signed_message = signed_message
136
+ end
87
137
 
88
- generate_signature(sender_id, recipient_id, protocol_version, message)
138
+ def signed_message_string
139
+ @signed_message_string ||= build_signed_message.to_json
89
140
  end
90
141
 
91
- # payment:: Google Pay token as a ruby Hash
92
- # signing_key:: OpenSSL::PKEY::EC
93
- # recipient:: OpenSSL::PKey::EC
94
- # signed_message:: Pass a customized message to sign as signed messaged.
95
- def self.generate_token_ecv1(payment, signing_key, recipient, signed_message = nil)
96
- cipher = OpenSSL::Cipher::AES128.new(:CTR)
97
- signed_message ||= JSON.unparse(encrypt(JSON.unparse(payment), recipient, cipher))
98
- signature_string = signature_string(signed_message)
142
+ def build_signed_key
143
+ return @signed_key if @signed_key
144
+ ensure_intermediate_key
99
145
 
100
- {
101
- 'protocolVersion' => 'ECv1',
102
- 'signature' => sign(signing_key, signature_string),
103
- 'signedMessage' => signed_message,
146
+ if @intermediate_key.private_key? || @intermediate_key.public_key?
147
+ public_key = eckey_to_public(@intermediate_key)
148
+ else
149
+ fail 'Intermediate key must be public and private key'
150
+ end
151
+
152
+ default_key_value = Base64.strict_encode64(public_key.to_der)
153
+ default_key_expiration = "#{Time.now.to_i + 3600}000"
154
+
155
+ @signed_key = {
156
+ 'keyExpiration' => @key_expiration || default_key_expiration,
157
+ 'keyValue' => @key_value || default_key_value,
104
158
  }
105
159
  end
106
160
 
107
- def self.generate_token_ecv2(payment, signing_key, intermediate_key, recipient,
108
- signed_message: nil, expire_time: "#{Time.now.to_i + 3600}000")
109
- cipher = OpenSSL::Cipher::AES256.new(:CTR)
110
- signed_message ||= JSON.unparse(encrypt(JSON.unparse(payment), recipient, cipher))
111
- sig = signature_string(signed_message, protocol_version: 'ECv2')
161
+ def signed_key_string
162
+ @signed_key_string ||= build_signed_key.to_json
163
+ end
112
164
 
113
- intermediate_pub = OpenSSL::PKey::EC.new(EC_CURVE)
114
- intermediate_pub.public_key = intermediate_key.public_key
165
+ def ensure_root_key
166
+ @root_key ||= OpenSSL::PKey::EC.new(EC_CURVE).generate_key
167
+ end
115
168
 
116
- signed_key = JSON.unparse(
117
- 'keyExpiration' => expire_time,
118
- 'keyValue' => Base64.strict_encode64(intermediate_pub.to_der)
119
- )
169
+ def ensure_intermediate_key
170
+ @intermediate_key ||= OpenSSL::PKey::EC.new(EC_CURVE).generate_key
171
+ end
120
172
 
121
- ik_signature_string = generate_signature('Google', 'ECv2', signed_key)
122
- signatures = [sign(signing_key, ik_signature_string)]
173
+ def build_signature
174
+ return @signature if @signature
175
+ key = case @protocol_version
176
+ when :ECv1
177
+ ensure_root_key
178
+ when :ECv2
179
+ ensure_intermediate_key
180
+ end
181
+
182
+ signature_string =
183
+ signed_string_message = ['Google',
184
+ recipient_id,
185
+ @protocol_version.to_s,
186
+ signed_message_string].map do |str|
187
+ [str.length].pack('V') + str
188
+ end.join
189
+ @signature = sign(key, signature_string)
190
+ end
123
191
 
124
- {
125
- 'protocolVersion' => 'ECv2',
126
- 'signature' => sign(intermediate_key, sig),
127
- 'signedMessage' => signed_message,
128
- 'intermediateSigningKey' => {
129
- 'signedKey' => signed_key,
130
- 'signatures' => signatures,
131
- },
192
+ def build_signatures
193
+ return @signatures if @signatures
194
+
195
+ signature_string =
196
+ signed_key_signature = ['Google', 'ECv2', signed_key_string].map do |str|
197
+ [str.to_s.length].pack('V') + str.to_s
198
+ end.join
199
+
200
+ @signatures = [sign(ensure_root_key, signature_string)]
201
+ end
202
+
203
+ def build_token
204
+ return @token if @token
205
+ res = {
206
+ 'protocolVersion' => @protocol_version.to_s,
207
+ 'signedMessage' => @signed_message || signed_message_string,
208
+ 'signature' => build_signature,
132
209
  }
210
+
211
+ if @protocol_version == :ECv2
212
+ intermediate = {
213
+ 'intermediateSigningKey' => @intermediate_signing_key || {
214
+ 'signedKey' => signed_key_string,
215
+ 'signatures' => build_signatures,
216
+ }
217
+ }
218
+
219
+ res.merge!(intermediate)
220
+ end
221
+
222
+ @token = res
223
+ end
224
+
225
+ def recipient_id
226
+ @recipient_id ||= DEFAULTS[:recipient_id]
227
+ end
228
+
229
+ def shared_secret
230
+ return Base64.strict_encode64(@shared_secret) if @shared_secret
231
+ @shared_secret ||= Random.new.bytes(32)
232
+ shared_secret
133
233
  end
134
234
  end
@@ -1,7 +1,7 @@
1
1
  require 'openssl'
2
2
  require 'hkdf'
3
3
 
4
- module AliquotPay
4
+ class AliquotPay
5
5
  class Util
6
6
  def self.generate_ephemeral_key
7
7
  OpenSSL::PKey::EC.new(AliquotPay::EC_CURVE).generate_key
@@ -11,18 +11,27 @@ module AliquotPay
11
11
  private_key.dh_compute_key(public_key)
12
12
  end
13
13
 
14
- def self.derive_keys(ephemeral_public_key, shared_secret, info, length: 32)
14
+ def self.derive_keys(ephemeral_public_key, shared_secret, info, protocol_version = :ECv2)
15
+ case protocol_version
16
+ when :ECv1
17
+ key_length = 16
18
+ when :ECv2
19
+ key_length = 32
20
+ else
21
+ raise StandardError, "invalid protocol_version #{protocol_version}"
22
+ end
23
+
15
24
  input_keying_material = ephemeral_public_key + shared_secret
16
25
  if OpenSSL.const_defined?(:KDF) && OpenSSL::KDF.respond_to?(:hkdf)
17
26
  h = OpenSSL::Digest::SHA256.new
18
- hbytes = OpenSSL::KDF.hkdf(input_keying_material, hash: h, salt: '', length: length * 2, info: info)
27
+ hbytes = OpenSSL::KDF.hkdf(input_keying_material, hash: h, salt: '', length: key_length * 2, info: info)
19
28
  else
20
- hbytes = HKDF.new(input_keying_material, algorithm: 'SHA256', info: info).next_bytes(length * 2)
29
+ hbytes = HKDF.new(input_keying_material, algorithm: 'SHA256', info: info).next_bytes(key_length * 2)
21
30
  end
22
31
 
23
32
  {
24
- aes_key: hbytes[0..length - 1],
25
- mac_key: hbytes[length..2 * length],
33
+ aes_key: hbytes[0, key_length],
34
+ mac_key: hbytes[key_length, key_length],
26
35
  }
27
36
  end
28
37
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aliquot-pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clearhaus
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-11 00:00:00.000000000 Z
11
+ date: 2020-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hkdf
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aliquot
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +52,7 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '3'
41
- description:
55
+ description:
42
56
  email: hello@clearhaus.com
43
57
  executables: []
44
58
  extensions: []
@@ -50,7 +64,7 @@ homepage: https://github.com/clearhaus/aliquot-pay
50
64
  licenses:
51
65
  - MIT
52
66
  metadata: {}
53
- post_install_message:
67
+ post_install_message:
54
68
  rdoc_options: []
55
69
  require_paths:
56
70
  - lib
@@ -65,9 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
79
  - !ruby/object:Gem::Version
66
80
  version: '0'
67
81
  requirements: []
68
- rubyforge_project:
69
- rubygems_version: 2.7.7
70
- signing_key:
82
+ rubygems_version: 3.1.4
83
+ signing_key:
71
84
  specification_version: 4
72
85
  summary: Generates Google Pay test dummy tokens
73
86
  test_files: []