diff --git a/lib/adyen-ruby-api-library.rb b/lib/adyen-ruby-api-library.rb index 846ccc8e..0f6a4a2c 100644 --- a/lib/adyen-ruby-api-library.rb +++ b/lib/adyen-ruby-api-library.rb @@ -8,6 +8,7 @@ require_relative "adyen/services/recurring" require_relative "adyen/services/marketpay" require_relative "adyen/services/service" +require_relative "adyen/utils/hmac_validator" # add snake case to camel case converter to String # to convert rubinic method names to Adyen API methods diff --git a/lib/adyen/utils/hmac_validator.rb b/lib/adyen/utils/hmac_validator.rb new file mode 100644 index 00000000..75a02702 --- /dev/null +++ b/lib/adyen/utils/hmac_validator.rb @@ -0,0 +1,48 @@ +module Adyen + module Utils + class HmacValidator + HMAC_ALGORITHM = 'sha256'.freeze + DATA_SEPARATOR = ':'.freeze + NOTIFICATION_VALIDATION_KEYS = %w[ + pspReference originalReference merchantAccountCode merchantReference + amount.value amount.currency eventCode success + ].freeze + + def valid_notification_hmac?(notification_request_item, hmac_key) + expected_sign = calculate_notification_hmac(notification_request_item, hmac_key) + merchant_sign = fetch(notification_request_item, 'additionalData.hmacSignature') + + expected_sign == merchant_sign + end + + def calculate_notification_hmac(notification_request_item, hmac_key) + data = data_to_sign(notification_request_item) + + Base64.strict_encode64(OpenSSL::HMAC.digest(HMAC_ALGORITHM, [hmac_key].pack('H*'), data)) + end + + def data_to_sign(notification_request_item) + NOTIFICATION_VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s } + .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') } + .join(DATA_SEPARATOR) + end + + private + + def fetch(hash, keys) + value = hash + + keys.to_s.split('.').each do |key| + value = if key.to_i.to_s == key + value[key.to_i] + else + value[key].nil? ? value[key.to_sym] : value[key] + end + break if value.nil? + end + + value + end + end + end +end diff --git a/spec/utils/hmac_validator_spec.rb b/spec/utils/hmac_validator_spec.rb new file mode 100644 index 00000000..9fcb258e --- /dev/null +++ b/spec/utils/hmac_validator_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +RSpec.describe Adyen::Utils::HmacValidator do + let(:validator) { described_class.new } + let(:key) { '44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056' } + let(:expected_sign) { 'coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=' } + let(:notification_request_item) do + { + additionalData: { + hmacSignature: expected_sign + }, + amount: { + value: 1130, + currency: 'EUR' + }, + pspReference: '7914073381342284', + eventCode: 'AUTHORISATION', + merchantAccountCode: 'TestMerchant', + merchantReference: 'TestPayment-1407325143704', + paymentMethod: 'visa', + success: 'true' + } + end + + describe 'HMAC Validator' do + it 'should get correct data' do + data_to_sign = validator.data_to_sign(notification_request_item) + expect(data_to_sign).to eq '7914073381342284::TestMerchant:TestPayment-1407325143704:1130:EUR:AUTHORISATION:true' + end + + it 'should get correct data with escaped characters' do + notification_request_item['merchantAccountCode'] = 'Test:\\Merchant' + data_to_sign = validator.data_to_sign(notification_request_item) + expect(data_to_sign).to eq '7914073381342284::Test\\:\\Merchant:TestPayment-1407325143704:1130:EUR:AUTHORISATION:true' + end + + it 'should encrypt properly' do + encrypted = validator.calculate_notification_hmac(notification_request_item, key) + expect(encrypted).to eq expected_sign + end + + it 'should have a valid hmac' do + expect(validator.valid_notification_hmac?(notification_request_item, key)).to be true + end + + it 'should have an invalid hmac' do + notification_request_item['additionalData'] = { 'hmacSignature' => 'invalidHMACsign' } + + expect(validator.valid_notification_hmac?(notification_request_item, key)).to be false + end + end +end