aliquot 2.0.0 → 2.1.1

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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aliquot/validator.rb +179 -93
  3. metadata +57 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4b575c575e5104e23692cbf9ddf45fd40555862d306a88995d33c7c2fa2085b
4
- data.tar.gz: b2be001378fea01010ae490230d8cd14c0767b6fec3155a7a4d6c24c95262c7d
3
+ metadata.gz: 340c8e93be8733d7b403356c5d7b85f0f1b69466e8492f6750875f7d1b4b0332
4
+ data.tar.gz: 27bb764e594448851ddceaaf0e65afd0a8c7916b82cf65514f077ad67d65ac56
5
5
  SHA512:
6
- metadata.gz: 41e6616072916a6bf54baa76e62e8e9ccd07afc5056f7bcbf3886292d941f66c451f434924837e667592543184c91089f7032b25a1bb9daf646d9a69ef647529
7
- data.tar.gz: adfe0fe58146dd4824c1677a482e948b4405973d6c18b7bcd31f516d446921f3b6266b8600148ec05b7c03ba78650c7f33de39f100e93eed8587abcee28999c2
6
+ metadata.gz: 70e3dfd919b5b2dde76eb7025a6627b46f2462096a7a9ce747764315c8b612104be97a38c7c4e8f7b2d5927e807adf992dc4bd7c145d5e091c6a699c5fae4830
7
+ data.tar.gz: 58c0275b221f378289d417131eb3e84039be05183b09e0e00cdb0b5ad8450d59c28d859ec5242544ce6eedbd0b0a933b6ad82e4e3bd0fb3ae0d62041e9e44a8d
@@ -9,143 +9,221 @@ module Aliquot
9
9
  module Validator
10
10
  # Verified according to:
11
11
  # https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography#payment-method-token-structure
12
- module Predicates
13
- include Dry::Logic::Predicates
14
-
15
- CUSTOM_PREDICATE_ERRORS = {
16
- base64?: 'must be Base64',
17
- pan?: 'must be a pan',
18
- ec_public_key?: 'must be an EC public key',
19
- eci?: 'must be an ECI',
20
- json_string?: 'must be valid JSON',
21
- integer_string?: 'must be string encoded integer',
22
- month?: 'must be a month (1..12)',
23
- year?: 'must be a year (2000..3000)',
24
- base64_asn1?: 'must be base64 encoded asn1 value',
25
- json_object?: 'must be a JSON object',
26
-
27
- authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS requires eciIndicator',
28
- authMethodCard: 'eciIndicator/cryptogram must be omitted when PAN_ONLY',
29
- }.freeze
30
-
31
- # Support Ruby 2.3, but use the faster #match? when available.
32
- match_b = ''.respond_to?(:match?) ? ->(s, re) { s.match?(re) } : ->(s, re) { !!(s =~ re) }
33
-
34
- def self.to_bool(lbd)
35
- lbd.call
36
- true
37
- rescue
38
- false
39
- end
40
12
 
41
- predicate(:base64?) do |x|
42
- str?(x) &&
43
- match_b.call(x, /\A[=A-Za-z0-9+\/]*\z/) && # allowable chars
44
- x.length.remainder(4).zero? && # multiple of 4
45
- !match_b.call(x, /=[^$=]/) && # may only end with ='s
46
- !match_b.call(x, /===/) # at most 2 ='s
47
- end
13
+ CUSTOM_ERRORS = {
14
+ base64?: 'must be Base64',
15
+ pan?: 'must be a PAN',
16
+ ec_public_key?: 'must be an EC public key',
17
+ eci?: 'must be an ECI',
18
+ integer_string?: 'must be string encoded integer',
19
+ month?: 'must be a month (1..12)',
20
+ year?: 'must be a year (2000..3000)',
21
+ base64_asn1?: 'must be base64-encoded ANS.1 value',
22
+ json?: 'must be valid JSON',
23
+
24
+ is_authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS requires eciIndicator',
25
+ is_authMethodCard: 'eciIndicator/cryptogram must be omitted when PAN_ONLY',
26
+ }.freeze
27
+
28
+ def self.base64_check(value)
29
+ /\A[=A-Za-z0-9+\/]*\z/.match?(value) && # allowable chars
30
+ value.length.remainder(4).zero? && # multiple of 4
31
+ !/=[^$=]/.match?(value) && # may only end with ='s
32
+ !/===/.match?(value) # at most 2 ='s
33
+ end
48
34
 
49
- # We should figure out how strict we should be. Hopefully we can discard
50
- # the above Base64? predicate and use the following simpler one:
51
- #predicate(:strict_base64?) { |x| !!Base64.strict_decode64(x) rescue false }
35
+ Dry::Validation.register_macro(:base64?) do
36
+ if key?
37
+ unless Aliquot::Validator.base64_check(value)
38
+ key.failure(CUSTOM_ERRORS[:base64?])
39
+ end
40
+ end
41
+ end
52
42
 
53
- predicate(:pan?) { |x| match_b.call(x, /\A[1-9][0-9]{11,18}\z/) }
43
+ def self.ans1_check(value)
44
+ OpenSSL::ASN1.decode(Base64.strict_decode64(value)) rescue false
45
+ end
54
46
 
55
- predicate(:eci?) { |x| str?(x) && match_b.call(x, /\A\d{1,2}\z/) }
47
+ Dry::Validation.register_macro(:base64_asn1?) do
48
+ if key?
49
+ unless Aliquot::Validator.ans1_check(value)
50
+ key.failure(CUSTOM_ERRORS[:base64_asn1?])
51
+ end
52
+ end
53
+ end
56
54
 
57
- predicate(:ec_public_key?) { |x| base64?(x) && OpenSSL::PKey::EC.new(Base64.decode64(x)).check_key rescue false }
55
+ Dry::Validation.register_macro(:pan?) do
56
+ if key?
57
+ unless /\A[1-9][0-9]{11,18}\z/.match?(value)
58
+ key.failure(CUSTOM_ERRORS[:pan?])
59
+ end
60
+ end
61
+ end
58
62
 
59
- predicate(:json_string?) { |x| !!JSON.parse(x) rescue false }
63
+ Dry::Validation.register_macro(:ec_public_key?) do
64
+ if key?
65
+ begin
66
+ OpenSSL::PKey::EC.new(Base64.decode64(value)).check_key
67
+ rescue
68
+ key.failure(CUSTOM_ERRORS[:ec_public_key?])
69
+ end
70
+ end
71
+ end
60
72
 
61
- predicate(:integer_string?) { |x| str?(x) && match_b.call(x, /\A\d+\z/) }
73
+ Dry::Validation.register_macro(:eci?) do
74
+ if key?
75
+ unless /\A\d{1,2}\z/.match?(value)
76
+ key.failure(CUSTOM_ERRORS[:eci?])
77
+ end
78
+ end
79
+ end
62
80
 
63
- predicate(:month?) { |x| x.between?(1, 12) }
81
+ Dry::Validation.register_macro(:integer_string?) do
82
+ if key?
83
+ unless /\A\d+\z/.match?(value)
84
+ key.failure(CUSTOM_ERRORS[:integer_string?])
85
+ end
86
+ end
87
+ end
64
88
 
65
- predicate(:year?) { |x| x.between?(2000, 3000) }
89
+ Dry::Validation.register_macro(:json?) do
90
+ if key?
91
+ json = JSON.parse(value) rescue false
92
+ key.failure(CUSTOM_ERRORS[:json?]) unless json
93
+ end
94
+ end
66
95
 
67
- predicate(:base64_asn1?) { |x| OpenSSL::ASN1.decode(Base64.strict_decode64(x)) rescue false }
96
+ Dry::Validation.register_macro(:month?) do
97
+ if key?
98
+ unless value.between?(1, 12)
99
+ key.failure(CUSTOM_ERRORS[:month?])
100
+ end
101
+ end
102
+ end
68
103
 
69
- predicate(:json_object?) { |x| hash?(x) }
104
+ Dry::Validation.register_macro(:year?) do
105
+ if key?
106
+ unless value.between?(2000, 3000)
107
+ key.failure(CUSTOM_ERRORS[:year?])
108
+ end
109
+ end
70
110
  end
71
111
 
72
- # Base for DRY-Validation schemas used in Aliquot.
73
- class BaseSchema < Dry::Validation::Schema::JSON
74
- predicates(Predicates)
75
- def self.messages
76
- super.merge(en: { errors: Predicates::CUSTOM_PREDICATE_ERRORS })
112
+ class SignedKeyContract < Dry::Validation::Contract
113
+ json do
114
+ required(:keyExpiration).filled(:str?)
115
+ required(:keyValue).filled(:str?)
77
116
  end
117
+ rule(:keyExpiration).validate(:integer_string?)
118
+ rule(:keyValue).validate(:ec_public_key?)
78
119
  end
79
120
 
80
- # Schema used for the 'intermediateSigningKey' hash included in ECv2.
81
- IntermediateSigningKeySchema = Dry::Validation.Schema(BaseSchema) do
82
- required(:signedKey).filled(:str?, :json_string?)
121
+ SignedKeySchema = SignedKeyContract.new
83
122
 
84
- required(:signatures).filled(:array?) { each { base64? & base64_asn1? } }
123
+ # Schema used for the 'intermediateSigningKey' hash included in ECv2.
124
+ class IntermediateSigningKeyContract < Dry::Validation::Contract
125
+ json do
126
+ required(:signedKey).filled(:str?)
127
+ required(:signatures).array(:str?)
128
+ end
129
+ rule(:signedKey).validate(:json?)
130
+ rule(:signatures).each do
131
+ key.failure('must be Base64') unless Aliquot::Validator.base64_check(value) &&
132
+ Aliquot::Validator.ans1_check(value)
133
+ end
85
134
  end
86
135
 
87
- SignedKeySchema = Dry::Validation.Schema(BaseSchema) do
88
- required(:keyExpiration).filled(:integer_string?)
89
- required(:keyValue).filled(:ec_public_key?)
90
- end
136
+ IntermediateSigningKeySchema = IntermediateSigningKeyContract.new
91
137
 
92
138
  # DRY-Validation schema for Google Pay token
93
- TokenSchema = Dry::Validation.Schema(BaseSchema) do
94
- required(:signature).filled(:str?, :base64?, :base64_asn1?)
95
-
96
- required(:protocolVersion).filled(:str?).when(eql?: 'ECv2') do
97
- required(:intermediateSigningKey)
139
+ class TokenContract < Dry::Validation::Contract
140
+ json do
141
+ required(:signature).filled(:str?)
142
+ required(:signedMessage).filled(:str?)
143
+ required(:protocolVersion).filled(:str?)
144
+ optional(:intermediateSigningKey).hash(IntermediateSigningKeyContract.new.schema)
98
145
  end
146
+ rule(:signature).validate(:base64?, :base64_asn1?)
147
+ rule(:signedMessage).validate(:json?)
99
148
 
100
- required(:signedMessage).filled(:str?, :json_string?)
101
-
102
- optional(:intermediateSigningKey).value(:json_object?) { schema(IntermediateSigningKeySchema) }
149
+ rule(:intermediateSigningKey) do
150
+ key.failure('is missing') if 'ECv2'.eql?(values[:protocolVersion]) &&
151
+ values[:intermediateSigningKey].nil?
152
+ end
103
153
  end
104
154
 
155
+ TokenSchema = TokenContract.new
156
+
105
157
  # DRY-Validation schema for signedMessage component Google Pay token
106
- SignedMessageSchema = Dry::Validation.Schema(BaseSchema) do
107
- required(:encryptedMessage).filled(:str?, :base64?)
108
- required(:ephemeralPublicKey).filled(:str?, :base64?)
109
- required(:tag).filled(:str?, :base64?)
158
+ class SignedMessageContract < Dry::Validation::Contract
159
+ json do
160
+ required(:encryptedMessage).filled(:str?)
161
+ required(:ephemeralPublicKey).filled(:str?)
162
+ required(:tag).filled(:str?)
163
+ end
164
+ rule(:encryptedMessage).validate(:base64?)
165
+ rule(:ephemeralPublicKey).validate(:base64?)
166
+ rule(:tag).validate(:base64?)
110
167
  end
111
168
 
112
- # DRY-Validation schema for paymentMethodDetails component Google Pay token
113
- PaymentMethodDetailsSchema = Dry::Validation.Schema(BaseSchema) do
114
- required(:pan).filled(:integer_string?, :pan?)
115
- required(:expirationMonth).filled(:int?, :month?)
116
- required(:expirationYear).filled(:int?, :year?)
117
- required(:authMethod).filled(:str?, included_in?: %w[PAN_ONLY CRYPTOGRAM_3DS])
118
-
119
- optional(:cryptogram).filled(:str?)
120
- optional(:eciIndicator).filled(:str?, :eci?)
169
+ SignedMessageSchema = SignedMessageContract.new
121
170
 
122
- rule(cryptogram: %i[authMethod cryptogram]) do |method, cryptogram|
123
- method.eql?('CRYPTOGRAM_3DS') > required(:cryptogram)
171
+ # DRY-Validation schema for paymentMethodDetails component Google Pay token
172
+ class PaymentMethodDetailsContract < Dry::Validation::Contract
173
+ json do
174
+ required(:pan).filled(:str?)
175
+ required(:expirationMonth).filled(:int?)
176
+ required(:expirationYear).filled(:int?)
177
+ required(:authMethod).filled(:str?, included_in?: %w[PAN_ONLY CRYPTOGRAM_3DS])
178
+
179
+ optional(:cryptogram).filled(:str?)
180
+ optional(:eciIndicator).filled(:str?)
181
+ end
182
+ rule(:pan).validate(:integer_string?, :pan?)
183
+ rule(:expirationMonth).validate(:month?)
184
+ rule(:expirationYear).validate(:year?)
185
+ rule(:eciIndicator).validate(:eci?)
186
+
187
+ rule(:cryptogram) do
188
+ key.failure('is missing') if 'CRYPTOGRAM_3DS'.eql?(values[:authMethod]) &&
189
+ values[:cryptogram].nil?
124
190
  end
125
191
 
126
- rule(eciIndicator: %i[authMethod eciIndicator]) do |method, eci|
127
- method.eql?('PAN_ONLY').then(eci.none?)
192
+ rule(:cryptogram) do
193
+ key.failure('cannot be defined') if 'PAN_ONLY'.eql?(values[:authMethod]) &&
194
+ !values[:cryptogram].nil?
128
195
  end
129
196
 
130
- rule(cryptogram: %i[authMethod cryptogram]) do |method, cryptogram|
131
- method.eql?('PAN_ONLY').then(cryptogram.none?)
197
+ rule(:eciIndicator) do
198
+ key.failure('cannot be defined') if 'PAN_ONLY'.eql?(values[:authMethod]) &&
199
+ !values[:eciIndicator].nil?
132
200
  end
133
201
  end
134
202
 
203
+ PaymentMethodDetailsSchema = PaymentMethodDetailsContract.new
204
+
135
205
  # DRY-Validation schema for encryptedMessage component Google Pay token
136
- EncryptedMessageSchema = Dry::Validation.Schema(BaseSchema) do
137
- required(:messageExpiration).filled(:str?, :integer_string?)
138
- required(:messageId).filled(:str?)
139
- required(:paymentMethod).filled(:str?, eql?: 'CARD')
140
- required(:paymentMethodDetails).value(:json_object?) { schema PaymentMethodDetailsSchema }
206
+ class EncryptedMessageContract < Dry::Validation::Contract
207
+ json do
208
+ required(:messageExpiration).filled(:str?)
209
+ required(:messageId).filled(:str?)
210
+ required(:paymentMethod).filled(:str?)
211
+ required(:paymentMethodDetails).filled(:hash).schema(PaymentMethodDetailsContract.schema)
212
+ end
213
+ rule(:messageExpiration).validate(:integer_string?)
214
+ rule(:paymentMethod) do
215
+ key.failure('must be equal to CARD') unless 'CARD'.eql?(value)
216
+ end
141
217
  end
142
218
 
219
+ EncryptedMessageSchema = EncryptedMessageContract.new
220
+
143
221
  module InstanceMethods
144
222
  attr_reader :output
145
223
 
146
224
  def validate
147
225
  @validation ||= @schema.call(@input)
148
- @output = @validation.output
226
+ @output = @validation.to_h
149
227
  return true if @validation.success?
150
228
  raise Aliquot::ValidationError, "validation error(s), #{errors_formatted}"
151
229
  end
@@ -176,7 +254,9 @@ module Aliquot
176
254
  # Class for validating a Google Pay token
177
255
  class Token
178
256
  include InstanceMethods
257
+
179
258
  class Error < ::Aliquot::Error; end
259
+
180
260
  def initialize(input)
181
261
  @input = input
182
262
  @schema = TokenSchema
@@ -186,7 +266,9 @@ module Aliquot
186
266
  # Class for validating the SignedMessage component of a Google Pay token
187
267
  class SignedMessage
188
268
  include InstanceMethods
269
+
189
270
  class Error < ::Aliquot::Error; end
271
+
190
272
  def initialize(input)
191
273
  @input = input
192
274
  @schema = SignedMessageSchema
@@ -196,7 +278,9 @@ module Aliquot
196
278
  # Class for validating the encryptedMessage component of a Google Pay token
197
279
  class EncryptedMessageValidator
198
280
  include InstanceMethods
281
+
199
282
  class Error < ::Aliquot::Error; end
283
+
200
284
  def initialize(input)
201
285
  @input = input
202
286
  @schema = EncryptedMessageSchema
@@ -205,7 +289,9 @@ module Aliquot
205
289
 
206
290
  class SignedKeyValidator
207
291
  include InstanceMethods
292
+
208
293
  class Error < ::Aliquot::Error; end
294
+
209
295
  def initialize(input)
210
296
  @input = input
211
297
  @schema = SignedKeySchema
metadata CHANGED
@@ -1,35 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aliquot
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clearhaus
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-validation
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 0.11.0
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '0.13'
19
+ version: '1.8'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 0.11.0
30
- - - "<"
24
+ - - "~>"
31
25
  - !ruby/object:Gem::Version
32
- version: '0.13'
26
+ version: '1.8'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: excon
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -64,14 +58,42 @@ dependencies:
64
58
  requirements:
65
59
  - - "~>"
66
60
  - !ruby/object:Gem::Version
67
- version: 2.0.0
61
+ version: 2.1.1
68
62
  type: :development
69
63
  prerelease: false
70
64
  version_requirements: !ruby/object:Gem::Requirement
71
65
  requirements:
72
66
  - - "~>"
73
67
  - !ruby/object:Gem::Version
74
- version: 2.0.0
68
+ version: 2.1.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
75
97
  - !ruby/object:Gem::Dependency
76
98
  name: rspec
77
99
  requirement: !ruby/object:Gem::Requirement
@@ -86,7 +108,21 @@ dependencies:
86
108
  - - "~>"
87
109
  - !ruby/object:Gem::Version
88
110
  version: '3'
89
- description:
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
90
126
  email: hello@clearhaus.com
91
127
  executables: []
92
128
  extensions: []
@@ -100,23 +136,23 @@ homepage: https://github.com/clearhaus/aliquot
100
136
  licenses:
101
137
  - MIT
102
138
  metadata: {}
103
- post_install_message:
139
+ post_install_message:
104
140
  rdoc_options: []
105
141
  require_paths:
106
142
  - lib
107
143
  required_ruby_version: !ruby/object:Gem::Requirement
108
144
  requirements:
109
- - - ">="
145
+ - - "~>"
110
146
  - !ruby/object:Gem::Version
111
- version: '0'
147
+ version: '2.7'
112
148
  required_rubygems_version: !ruby/object:Gem::Requirement
113
149
  requirements:
114
150
  - - ">="
115
151
  - !ruby/object:Gem::Version
116
152
  version: '0'
117
153
  requirements: []
118
- rubygems_version: 3.1.4
119
- signing_key:
154
+ rubygems_version: 3.1.6
155
+ signing_key:
120
156
  specification_version: 4
121
157
  summary: Validates Google Pay tokens
122
158
  test_files: []