zai_payment 1.1.0 → 1.2.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.
data/examples/webhooks.md CHANGED
@@ -16,6 +16,228 @@ ZaiPayment.configure do |config|
16
16
  end
17
17
  ```
18
18
 
19
+ ## Webhook Security: Complete Setup Guide
20
+
21
+ ### Step 1: Generate and Register a Secret Key
22
+
23
+ Before setting up webhooks, you should establish a secure secret key for signature verification:
24
+
25
+ ```ruby
26
+ require 'securerandom'
27
+
28
+ # Generate a cryptographically secure secret key (minimum 32 bytes)
29
+ secret_key = SecureRandom.alphanumeric(32)
30
+ # Example output: "aB3xYz9mKpQrTuVwXy2zAbCdEfGhIjKl"
31
+
32
+ # Store this in your environment variables or secure vault
33
+ # NEVER commit this to version control!
34
+ puts "Add this to your environment variables:"
35
+ puts "ZAI_WEBHOOK_SECRET=#{secret_key}"
36
+
37
+ # Register the secret key with Zai
38
+ response = ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
39
+
40
+ if response.success?
41
+ puts "✅ Secret key registered successfully with Zai!"
42
+ puts "Store this key securely - you'll need it to verify webhook signatures"
43
+ else
44
+ puts "❌ Failed to register secret key"
45
+ end
46
+ ```
47
+
48
+ ### Step 2: Create a Webhook
49
+
50
+ Now create a webhook to receive notifications:
51
+
52
+ ```ruby
53
+ response = ZaiPayment.webhooks.create(
54
+ url: 'https://your-app.com/webhooks/zai',
55
+ object_type: 'transactions',
56
+ enabled: true,
57
+ description: 'Production webhook for transaction updates'
58
+ )
59
+
60
+ webhook = response.data
61
+ puts "Created webhook: #{webhook['id']}"
62
+ ```
63
+
64
+ ### Step 3: Implement Webhook Endpoint
65
+
66
+ Here's a complete Rails controller example with signature verification:
67
+
68
+ ```ruby
69
+ # app/controllers/webhooks_controller.rb
70
+ class WebhooksController < ApplicationController
71
+ # Skip CSRF token verification for webhook endpoints
72
+ skip_before_action :verify_authenticity_token
73
+
74
+ # Add basic rate limiting (if using Rack::Attack or similar)
75
+ # throttle('webhooks/ip', limit: 100, period: 1.minute)
76
+
77
+ def zai_webhook
78
+ # Read the raw request body - IMPORTANT: Don't parse it first!
79
+ payload = request.body.read
80
+ signature_header = request.headers['Webhooks-signature']
81
+ secret_key = ENV['ZAI_WEBHOOK_SECRET']
82
+
83
+ # Verify the signature
84
+ unless verify_webhook_signature(payload, signature_header, secret_key)
85
+ Rails.logger.warn "Invalid webhook signature received from #{request.remote_ip}"
86
+ return render json: { error: 'Invalid signature' }, status: :unauthorized
87
+ end
88
+
89
+ # Parse and process the webhook
90
+ webhook_data = JSON.parse(payload)
91
+ process_webhook(webhook_data)
92
+
93
+ # Return 200 to acknowledge receipt
94
+ render json: { status: 'success' }, status: :ok
95
+ rescue JSON::ParserError => e
96
+ Rails.logger.error "Invalid JSON in webhook: #{e.message}"
97
+ render json: { error: 'Invalid JSON' }, status: :bad_request
98
+ rescue StandardError => e
99
+ Rails.logger.error "Webhook processing error: #{e.message}"
100
+ render json: { error: 'Processing error' }, status: :internal_server_error
101
+ end
102
+
103
+ private
104
+
105
+ def verify_webhook_signature(payload, signature_header, secret_key)
106
+ return false if signature_header.blank?
107
+
108
+ ZaiPayment.webhooks.verify_signature(
109
+ payload: payload,
110
+ signature_header: signature_header,
111
+ secret_key: secret_key,
112
+ tolerance: 300 # 5 minutes - adjust based on your needs
113
+ )
114
+ rescue ZaiPayment::Errors::ValidationError => e
115
+ Rails.logger.warn "Webhook signature validation failed: #{e.message}"
116
+ false
117
+ end
118
+
119
+ def process_webhook(data)
120
+ # Log the webhook for debugging
121
+ Rails.logger.info "Processing Zai webhook: #{data['event']}"
122
+
123
+ # Handle different webhook events
124
+ case data['event']
125
+ when 'transaction.created'
126
+ handle_transaction_created(data)
127
+ when 'transaction.updated'
128
+ handle_transaction_updated(data)
129
+ when 'transaction.completed'
130
+ handle_transaction_completed(data)
131
+ else
132
+ Rails.logger.info "Unhandled webhook event: #{data['event']}"
133
+ end
134
+ end
135
+
136
+ def handle_transaction_created(data)
137
+ # Your logic here
138
+ Rails.logger.info "Transaction created: #{data['transaction']['id']}"
139
+ end
140
+
141
+ def handle_transaction_updated(data)
142
+ # Your logic here
143
+ Rails.logger.info "Transaction updated: #{data['transaction']['id']}"
144
+ end
145
+
146
+ def handle_transaction_completed(data)
147
+ # Your logic here
148
+ Rails.logger.info "Transaction completed: #{data['transaction']['id']}"
149
+ end
150
+ end
151
+ ```
152
+
153
+ ### Step 4: Configure Routes
154
+
155
+ ```ruby
156
+ # config/routes.rb
157
+ Rails.application.routes.draw do
158
+ # Webhook endpoint
159
+ post '/webhooks/zai', to: 'webhooks#zai_webhook'
160
+ end
161
+ ```
162
+
163
+ ### Step 5: Test Your Webhook
164
+
165
+ Create a test to verify your implementation:
166
+
167
+ ```ruby
168
+ # spec/controllers/webhooks_controller_spec.rb
169
+ require 'rails_helper'
170
+
171
+ RSpec.describe WebhooksController, type: :controller do
172
+ let(:secret_key) { SecureRandom.alphanumeric(32) }
173
+ let(:webhook_payload) do
174
+ {
175
+ event: 'transaction.updated',
176
+ transaction: {
177
+ id: 'txn_123',
178
+ state: 'completed',
179
+ amount: 1000
180
+ }
181
+ }.to_json
182
+ end
183
+
184
+ before do
185
+ ENV['ZAI_WEBHOOK_SECRET'] = secret_key
186
+ end
187
+
188
+ describe 'POST #zai_webhook' do
189
+ context 'with valid signature' do
190
+ it 'processes the webhook successfully' do
191
+ timestamp = Time.now.to_i
192
+ signature = ZaiPayment::Resources::Webhook.new.generate_signature(
193
+ webhook_payload, secret_key, timestamp
194
+ )
195
+
196
+ request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
197
+ post :zai_webhook, body: webhook_payload
198
+
199
+ expect(response).to have_http_status(:ok)
200
+ expect(JSON.parse(response.body)['status']).to eq('success')
201
+ end
202
+ end
203
+
204
+ context 'with invalid signature' do
205
+ it 'rejects the webhook' do
206
+ timestamp = Time.now.to_i
207
+ request.headers['Webhooks-signature'] = "t=#{timestamp},v=invalid_signature"
208
+ post :zai_webhook, body: webhook_payload
209
+
210
+ expect(response).to have_http_status(:unauthorized)
211
+ end
212
+ end
213
+
214
+ context 'with missing signature header' do
215
+ it 'rejects the webhook' do
216
+ post :zai_webhook, body: webhook_payload
217
+
218
+ expect(response).to have_http_status(:unauthorized)
219
+ end
220
+ end
221
+
222
+ context 'with expired timestamp' do
223
+ it 'rejects old webhooks to prevent replay attacks' do
224
+ old_timestamp = Time.now.to_i - 600 # 10 minutes ago
225
+ signature = ZaiPayment::Resources::Webhook.new.generate_signature(
226
+ webhook_payload, secret_key, old_timestamp
227
+ )
228
+
229
+ request.headers['Webhooks-signature'] = "t=#{old_timestamp},v=#{signature}"
230
+ post :zai_webhook, body: webhook_payload
231
+
232
+ expect(response).to have_http_status(:unauthorized)
233
+ end
234
+ end
235
+ end
236
+ end
237
+ ```
238
+
239
+ ## Basic Webhook Operations
240
+
19
241
  ## List Webhooks
20
242
 
21
243
  ```ruby
@@ -144,3 +366,270 @@ response.headers # => Response headers
144
366
  response.status # => HTTP status code
145
367
  ```
146
368
 
369
+ ## Additional Webhook Security Examples
370
+
371
+ ### Generate Signature for Testing
372
+
373
+ You can generate signatures for testing your webhook implementation:
374
+
375
+ ```ruby
376
+ # Useful for integration tests or webhook simulation
377
+ payload = '{"event": "transaction.updated", "id": "txn_123"}'
378
+ secret_key = ENV['ZAI_WEBHOOK_SECRET']
379
+ timestamp = Time.now.to_i
380
+
381
+ webhook = ZaiPayment::Resources::Webhook.new
382
+ signature = webhook.generate_signature(payload, secret_key, timestamp)
383
+
384
+ puts "Signature header: t=#{timestamp},v=#{signature}"
385
+ ```
386
+
387
+ ### Verify Signature Manually
388
+
389
+ If you need more control over the verification process:
390
+
391
+ ```ruby
392
+ webhook = ZaiPayment::Resources::Webhook.new
393
+
394
+ begin
395
+ is_valid = webhook.verify_signature(
396
+ payload: request_body,
397
+ signature_header: request.headers['Webhooks-signature'],
398
+ secret_key: ENV['ZAI_WEBHOOK_SECRET'],
399
+ tolerance: 300
400
+ )
401
+
402
+ if is_valid
403
+ puts "✅ Webhook signature is valid"
404
+ else
405
+ puts "❌ Webhook signature is invalid"
406
+ end
407
+ rescue ZaiPayment::Errors::ValidationError => e
408
+ puts "⚠️ Validation error: #{e.message}"
409
+ end
410
+ ```
411
+
412
+ ### Sinatra Example
413
+
414
+ If you're using Sinatra instead of Rails:
415
+
416
+ ```ruby
417
+ require 'sinatra'
418
+ require 'json'
419
+ require 'zai_payment'
420
+
421
+ # Configure ZaiPayment
422
+ ZaiPayment.configure do |config|
423
+ config.environment = :prelive
424
+ config.client_id = ENV['ZAI_CLIENT_ID']
425
+ config.client_secret = ENV['ZAI_CLIENT_SECRET']
426
+ config.scope = ENV['ZAI_SCOPE']
427
+ end
428
+
429
+ post '/webhooks/zai' do
430
+ # Read raw request body
431
+ payload = request.body.read
432
+ signature_header = request.env['HTTP_WEBHOOKS_SIGNATURE']
433
+ secret_key = ENV['ZAI_WEBHOOK_SECRET']
434
+
435
+ # Verify signature
436
+ webhook = ZaiPayment::Resources::Webhook.new
437
+
438
+ begin
439
+ unless webhook.verify_signature(
440
+ payload: payload,
441
+ signature_header: signature_header,
442
+ secret_key: secret_key
443
+ )
444
+ halt 401, { error: 'Invalid signature' }.to_json
445
+ end
446
+
447
+ # Process webhook
448
+ data = JSON.parse(payload)
449
+ logger.info "Received webhook: #{data['event']}"
450
+
451
+ # Your processing logic here
452
+
453
+ status 200
454
+ { status: 'success' }.to_json
455
+ rescue ZaiPayment::Errors::ValidationError => e
456
+ logger.error "Webhook validation failed: #{e.message}"
457
+ halt 401, { error: e.message }.to_json
458
+ rescue StandardError => e
459
+ logger.error "Webhook processing error: #{e.message}"
460
+ halt 500, { error: 'Processing error' }.to_json
461
+ end
462
+ end
463
+ ```
464
+
465
+ ### Rack Middleware Example
466
+
467
+ Create reusable middleware for webhook verification:
468
+
469
+ ```ruby
470
+ # lib/middleware/zai_webhook_verifier.rb
471
+ module Middleware
472
+ class ZaiWebhookVerifier
473
+ def initialize(app, options = {})
474
+ @app = app
475
+ @secret_key = options[:secret_key] || ENV['ZAI_WEBHOOK_SECRET']
476
+ @tolerance = options[:tolerance] || 300
477
+ @webhook_path = options[:path] || '/webhooks/zai'
478
+ end
479
+
480
+ def call(env)
481
+ request = Rack::Request.new(env)
482
+
483
+ # Only verify requests to the webhook path
484
+ if request.path == @webhook_path && request.post?
485
+ unless verify_request(request)
486
+ return [401, { 'Content-Type' => 'application/json' },
487
+ [{ error: 'Invalid webhook signature' }.to_json]]
488
+ end
489
+ end
490
+
491
+ @app.call(env)
492
+ end
493
+
494
+ private
495
+
496
+ def verify_request(request)
497
+ body = request.body.read
498
+ request.body.rewind # Important: rewind for downstream processing
499
+
500
+ signature_header = request.env['HTTP_WEBHOOKS_SIGNATURE']
501
+ return false unless signature_header
502
+
503
+ webhook = ZaiPayment::Resources::Webhook.new
504
+ webhook.verify_signature(
505
+ payload: body,
506
+ signature_header: signature_header,
507
+ secret_key: @secret_key,
508
+ tolerance: @tolerance
509
+ )
510
+ rescue ZaiPayment::Errors::ValidationError
511
+ false
512
+ end
513
+ end
514
+ end
515
+
516
+ # Usage in config.ru or Rails application.rb:
517
+ use Middleware::ZaiWebhookVerifier,
518
+ secret_key: ENV['ZAI_WEBHOOK_SECRET'],
519
+ tolerance: 300,
520
+ path: '/webhooks/zai'
521
+ ```
522
+
523
+ ### Background Job Processing
524
+
525
+ For production systems, process webhooks asynchronously:
526
+
527
+ ```ruby
528
+ # app/controllers/webhooks_controller.rb
529
+ class WebhooksController < ApplicationController
530
+ skip_before_action :verify_authenticity_token
531
+
532
+ def zai_webhook
533
+ payload = request.body.read
534
+ signature_header = request.headers['Webhooks-signature']
535
+
536
+ # Quick verification
537
+ unless verify_signature(payload, signature_header)
538
+ return render json: { error: 'Invalid signature' }, status: :unauthorized
539
+ end
540
+
541
+ # Enqueue for background processing
542
+ ZaiWebhookJob.perform_later(payload)
543
+
544
+ # Return immediately
545
+ render json: { status: 'accepted' }, status: :accepted
546
+ end
547
+
548
+ private
549
+
550
+ def verify_signature(payload, signature_header)
551
+ ZaiPayment.webhooks.verify_signature(
552
+ payload: payload,
553
+ signature_header: signature_header,
554
+ secret_key: ENV['ZAI_WEBHOOK_SECRET']
555
+ )
556
+ rescue ZaiPayment::Errors::ValidationError
557
+ false
558
+ end
559
+ end
560
+
561
+ # app/jobs/zai_webhook_job.rb
562
+ class ZaiWebhookJob < ApplicationJob
563
+ queue_as :webhooks
564
+
565
+ # Retry logic for transient failures
566
+ retry_on StandardError, wait: :exponentially_longer, attempts: 5
567
+
568
+ def perform(payload)
569
+ data = JSON.parse(payload)
570
+
571
+ # Idempotent processing - check if already processed
572
+ return if WebhookEvent.exists?(external_id: data['id'])
573
+
574
+ # Process the webhook
575
+ WebhookEvent.create!(
576
+ external_id: data['id'],
577
+ event_type: data['event'],
578
+ payload: data,
579
+ processed_at: Time.current
580
+ )
581
+
582
+ # Handle event
583
+ case data['event']
584
+ when 'transaction.completed'
585
+ TransactionProcessor.process_completion(data['transaction'])
586
+ when 'transaction.failed'
587
+ TransactionProcessor.process_failure(data['transaction'])
588
+ end
589
+ end
590
+ end
591
+ ```
592
+
593
+ ### Idempotency Pattern
594
+
595
+ Ensure webhooks are processed only once:
596
+
597
+ ```ruby
598
+ # app/models/webhook_event.rb
599
+ class WebhookEvent < ApplicationRecord
600
+ # Columns: external_id, event_type, payload (jsonb), processed_at, created_at
601
+
602
+ validates :external_id, presence: true, uniqueness: true
603
+
604
+ def self.process_if_new(webhook_data)
605
+ # Use database constraint to ensure atomicity
606
+ transaction do
607
+ event = create!(
608
+ external_id: webhook_data['id'],
609
+ event_type: webhook_data['event'],
610
+ payload: webhook_data
611
+ )
612
+
613
+ yield event if block_given?
614
+
615
+ event.update!(processed_at: Time.current)
616
+ end
617
+ rescue ActiveRecord::RecordNotUnique
618
+ Rails.logger.info "Webhook already processed: #{webhook_data['id']}"
619
+ false
620
+ end
621
+ end
622
+
623
+ # Usage:
624
+ def process_webhook(data)
625
+ WebhookEvent.process_if_new(data) do |event|
626
+ # Your processing logic here
627
+ # This block only runs if the webhook is new
628
+ case event.event_type
629
+ when 'transaction.completed'
630
+ handle_completion(data)
631
+ end
632
+ end
633
+ end
634
+ ```
635
+
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
4
+ require 'base64'
5
+
3
6
  module ZaiPayment
4
7
  module Resources
5
8
  # Webhook resource for managing Zai webhooks
@@ -130,6 +133,102 @@ module ZaiPayment
130
133
  client.delete("/webhooks/#{webhook_id}")
131
134
  end
132
135
 
136
+ # Create a secret key for webhook signature verification
137
+ #
138
+ # @param secret_key [String] the secret key to use for HMAC signature generation
139
+ # Must be ASCII characters and at least 32 bytes in size
140
+ # @return [Response] the API response
141
+ #
142
+ # @example
143
+ # webhooks = ZaiPayment::Resources::Webhook.new
144
+ # secret_key = SecureRandom.alphanumeric(32)
145
+ # response = webhooks.create_secret_key(secret_key: secret_key)
146
+ #
147
+ # @see https://developer.hellozai.com/reference/createsecretkey
148
+ # @see https://developer.hellozai.com/docs/verify-webhook-signatures
149
+ def create_secret_key(secret_key:)
150
+ validate_presence!(secret_key, 'secret_key')
151
+ validate_secret_key!(secret_key)
152
+
153
+ body = { secret_key: secret_key }
154
+ client.post('/webhooks/secret_key', body: body)
155
+ end
156
+
157
+ # Verify webhook signature
158
+ #
159
+ # This method verifies that a webhook request came from Zai by validating
160
+ # the HMAC SHA256 signature in the Webhooks-signature header.
161
+ #
162
+ # @param payload [String] the raw request body (JSON string)
163
+ # @param signature_header [String] the Webhooks-signature header value
164
+ # @param secret_key [String] your secret key used for signature generation
165
+ # @param tolerance [Integer] maximum age of webhook in seconds (default: 300 = 5 minutes)
166
+ # @return [Boolean] true if signature is valid and within tolerance
167
+ # @raise [Errors::ValidationError] if signature is invalid or timestamp is outside tolerance
168
+ #
169
+ # @example
170
+ # # In your webhook endpoint (e.g., Rails controller)
171
+ # def webhook
172
+ # payload = request.body.read
173
+ # signature_header = request.headers['Webhooks-signature']
174
+ # secret_key = ENV['ZAI_WEBHOOK_SECRET']
175
+ #
176
+ # if ZaiPayment.webhooks.verify_signature(
177
+ # payload: payload,
178
+ # signature_header: signature_header,
179
+ # secret_key: secret_key
180
+ # )
181
+ # # Process webhook
182
+ # render json: { status: 'success' }
183
+ # else
184
+ # render json: { error: 'Invalid signature' }, status: :unauthorized
185
+ # end
186
+ # end
187
+ #
188
+ # @see https://developer.hellozai.com/docs/verify-webhook-signatures
189
+ def verify_signature(payload:, signature_header:, secret_key:, tolerance: 300)
190
+ validate_presence!(payload, 'payload')
191
+ validate_presence!(signature_header, 'signature_header')
192
+ validate_presence!(secret_key, 'secret_key')
193
+
194
+ # Extract timestamp and signature from header
195
+ timestamp, signatures = parse_signature_header(signature_header)
196
+
197
+ # Verify timestamp is within tolerance (prevent replay attacks)
198
+ verify_timestamp!(timestamp, tolerance)
199
+
200
+ # Generate expected signature
201
+ expected_signature = generate_signature(payload, secret_key, timestamp)
202
+
203
+ # Compare signatures using constant-time comparison
204
+ signatures.any? { |sig| secure_compare(expected_signature, sig) }
205
+ end
206
+
207
+ # Generate a signature for webhook verification
208
+ #
209
+ # This is a utility method that can be used for testing or generating
210
+ # signatures for webhook simulation.
211
+ #
212
+ # @param payload [String] the request body (JSON string)
213
+ # @param secret_key [String] the secret key
214
+ # @param timestamp [Integer] the Unix timestamp (defaults to current time)
215
+ # @return [String] the base64url-encoded HMAC SHA256 signature
216
+ #
217
+ # @example
218
+ # webhooks = ZaiPayment::Resources::Webhook.new
219
+ # signature = webhooks.generate_signature(
220
+ # '{"event": "status_updated"}',
221
+ # 'my_secret_key'
222
+ # )
223
+ #
224
+ # @see https://developer.hellozai.com/docs/verify-webhook-signatures
225
+ def generate_signature(payload, secret_key, timestamp = Time.now.to_i)
226
+ signed_payload = "#{timestamp}.#{payload}"
227
+ digest = OpenSSL::Digest.new('sha256')
228
+ hash = OpenSSL::HMAC.digest(digest, secret_key, signed_payload)
229
+ Base64.urlsafe_encode64(hash, padding: false)
230
+ end
231
+
133
232
  private
134
233
 
135
234
  def validate_id!(value, field_name)
@@ -152,6 +251,81 @@ module ZaiPayment
152
251
  rescue URI::InvalidURIError
153
252
  raise Errors::ValidationError, 'url must be a valid URL'
154
253
  end
254
+
255
+ def validate_secret_key!(secret_key)
256
+ # Check if it's ASCII
257
+ raise Errors::ValidationError, 'secret_key must contain only ASCII characters' unless secret_key.ascii_only?
258
+
259
+ # Check minimum length (32 bytes)
260
+ return unless secret_key.bytesize < 32
261
+
262
+ raise Errors::ValidationError, 'secret_key must be at least 32 bytes in size'
263
+ end
264
+
265
+ def parse_signature_header(header)
266
+ # Format: "t=1257894000,v=signature1,v=signature2"
267
+ parts = header.split(',').map(&:strip)
268
+
269
+ timestamp, signatures = extract_timestamp_and_signatures(parts)
270
+
271
+ validate_timestamp_presence!(timestamp)
272
+ validate_signatures_presence!(signatures)
273
+
274
+ [timestamp, signatures]
275
+ end
276
+
277
+ def extract_timestamp_and_signatures(parts)
278
+ timestamp = nil
279
+ signatures = []
280
+
281
+ parts.each do |part|
282
+ key, value = part.split('=', 2)
283
+ case key
284
+ when 't'
285
+ timestamp = value.to_i
286
+ when 'v'
287
+ signatures << value
288
+ end
289
+ end
290
+
291
+ [timestamp, signatures]
292
+ end
293
+
294
+ def validate_timestamp_presence!(timestamp)
295
+ return unless timestamp.nil? || timestamp.zero?
296
+
297
+ raise Errors::ValidationError, 'Invalid signature header: missing or invalid timestamp'
298
+ end
299
+
300
+ def validate_signatures_presence!(signatures)
301
+ raise Errors::ValidationError, 'Invalid signature header: missing signature' if signatures.empty?
302
+ end
303
+
304
+ def verify_timestamp!(timestamp, tolerance)
305
+ current_time = Time.now.to_i
306
+ time_diff = (current_time - timestamp).abs
307
+
308
+ return unless time_diff > tolerance
309
+
310
+ raise Errors::ValidationError,
311
+ "Webhook timestamp is outside tolerance (#{time_diff}s vs #{tolerance}s max). " \
312
+ 'This may be a replay attack.'
313
+ end
314
+
315
+ # Constant-time string comparison to prevent timing attacks
316
+ # Uses OpenSSL's secure_compare if available, otherwise falls back to manual comparison
317
+ def secure_compare(str_a, str_b)
318
+ return false unless str_a.bytesize == str_b.bytesize
319
+
320
+ if defined?(OpenSSL.fixed_length_secure_compare)
321
+ OpenSSL.fixed_length_secure_compare(str_a, str_b)
322
+ else
323
+ # Fallback for older Ruby versions
324
+ result = 0
325
+ str_a.bytes.zip(str_b.bytes) { |x, y| result |= x ^ y }
326
+ result.zero?
327
+ end
328
+ end
155
329
  end
156
330
  end
157
331
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZaiPayment
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end