aliquot 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []