aliquot-pay 0.10.0 → 2.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.
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: []