From 2d0ef82bcf52e388f552125a48e263428fea6ea5 Mon Sep 17 00:00:00 2001 From: Sander Tuin Date: Mon, 2 Dec 2019 16:46:20 +0100 Subject: [PATCH 1/3] Add HMAC validator --- lib/adyen-ruby-api-library.rb | 1 + lib/adyen/utils/hmac_validator.rb | 48 ++++++++++++++++++++++++++++ spec/utils/hmac_validator_spec.rb | 52 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 lib/adyen/utils/hmac_validator.rb create mode 100644 spec/utils/hmac_validator_spec.rb 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..647d3a19 --- /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 + VALIDATION_KEYS = %w[ + pspReference originalReference merchantAccountCode merchantReference + amount.value amount.currency eventCode success + ].freeze + + def validate(notification_request_item, hmac_key) + expected_sign = calculate_hmac(notification_request_item, hmac_key) + merchant_sign = fetch(notification_request_item, 'additionalData.hmacSignature') + + expected_sign == merchant_sign + end + + def calculate_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) + 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..cb43291d --- /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_hmac(notification_request_item, key) + expect(encrypted).to eq expected_sign + end + + it 'should have a valid hmac' do + expect(validator.validate(notification_request_item, key)).to be_truthy + end + + it 'should have an invalid hmac' do + notification_request_item['additionalData'] = { 'hmacSignature' => 'invalidHMACsign' } + + expect(validator.validate(notification_request_item, key)).to be_falsy + end + end +end From 4c8220ff1b84ca4e1fd26fd0e0e1224619f8ae15 Mon Sep 17 00:00:00 2001 From: Sander Tuin Date: Mon, 2 Dec 2019 17:29:59 +0100 Subject: [PATCH 2/3] Use different hash style --- spec/utils/hmac_validator_spec.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/utils/hmac_validator_spec.rb b/spec/utils/hmac_validator_spec.rb index cb43291d..aec436f7 100644 --- a/spec/utils/hmac_validator_spec.rb +++ b/spec/utils/hmac_validator_spec.rb @@ -6,19 +6,19 @@ let(:expected_sign) { 'coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=' } let(:notification_request_item) do { - "additionalData": { - "hmacSignature": expected_sign + additionalData: { + hmacSignature: expected_sign }, - "amount": { - "value": 1130, - "currency": 'EUR' + amount: { + value: 1130, + currency: 'EUR' }, - "pspReference": '7914073381342284', - "eventCode": 'AUTHORISATION', - "merchantAccountCode": 'TestMerchant', - "merchantReference": 'TestPayment-1407325143704', - "paymentMethod": 'visa', - "success": 'true' + pspReference: '7914073381342284', + eventCode: 'AUTHORISATION', + merchantAccountCode: 'TestMerchant', + merchantReference: 'TestPayment-1407325143704', + paymentMethod: 'visa', + success: 'true' } end From 53d9a03ab09d58927ec34e65d3d2acc1c5dc1ea7 Mon Sep 17 00:00:00 2001 From: Sander Tuin Date: Fri, 6 Dec 2019 10:42:05 +0100 Subject: [PATCH 3/3] Rename methods and variables to be similar across libs --- lib/adyen/utils/hmac_validator.rb | 14 +++++++------- spec/utils/hmac_validator_spec.rb | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/adyen/utils/hmac_validator.rb b/lib/adyen/utils/hmac_validator.rb index 647d3a19..75a02702 100644 --- a/lib/adyen/utils/hmac_validator.rb +++ b/lib/adyen/utils/hmac_validator.rb @@ -3,28 +3,28 @@ module Utils class HmacValidator HMAC_ALGORITHM = 'sha256'.freeze DATA_SEPARATOR = ':'.freeze - VALIDATION_KEYS = %w[ + NOTIFICATION_VALIDATION_KEYS = %w[ pspReference originalReference merchantAccountCode merchantReference amount.value amount.currency eventCode success ].freeze - def validate(notification_request_item, hmac_key) - expected_sign = calculate_hmac(notification_request_item, hmac_key) + 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_hmac(notification_request_item, hmac_key) + 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) - VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s } - .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') } - .join(DATA_SEPARATOR) + NOTIFICATION_VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s } + .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') } + .join(DATA_SEPARATOR) end private diff --git a/spec/utils/hmac_validator_spec.rb b/spec/utils/hmac_validator_spec.rb index aec436f7..9fcb258e 100644 --- a/spec/utils/hmac_validator_spec.rb +++ b/spec/utils/hmac_validator_spec.rb @@ -35,18 +35,18 @@ end it 'should encrypt properly' do - encrypted = validator.calculate_hmac(notification_request_item, key) + 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.validate(notification_request_item, key)).to be_truthy + 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.validate(notification_request_item, key)).to be_falsy + expect(validator.valid_notification_hmac?(notification_request_item, key)).to be false end end end