TransactionHashValidator.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\AuthorizenetAcceptjs\Gateway\Validator;
  8. use Magento\AuthorizenetAcceptjs\Gateway\Config;
  9. use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader;
  10. use Magento\Framework\Encryption\Helper\Security;
  11. use Magento\Payment\Gateway\Validator\AbstractValidator;
  12. use Magento\Payment\Gateway\Validator\ResultInterface;
  13. use Magento\Payment\Gateway\Validator\ResultInterfaceFactory;
  14. /**
  15. * Validates the transaction hash
  16. */
  17. class TransactionHashValidator extends AbstractValidator
  18. {
  19. /**
  20. * The error code for failed transaction hash verification
  21. */
  22. private const ERROR_TRANSACTION_HASH = 'ETHV';
  23. /**
  24. * @var SubjectReader
  25. */
  26. private $subjectReader;
  27. /**
  28. * @var Config
  29. */
  30. private $config;
  31. /**
  32. * @param ResultInterfaceFactory $resultFactory
  33. * @param SubjectReader $subjectReader
  34. * @param Config $config
  35. */
  36. public function __construct(
  37. ResultInterfaceFactory $resultFactory,
  38. SubjectReader $subjectReader,
  39. Config $config
  40. ) {
  41. parent::__construct($resultFactory);
  42. $this->subjectReader = $subjectReader;
  43. $this->config = $config;
  44. }
  45. /**
  46. * Validates the transaction hash matches the configured hash
  47. *
  48. * @param array $validationSubject
  49. * @return ResultInterface
  50. */
  51. public function validate(array $validationSubject): ResultInterface
  52. {
  53. $response = $this->subjectReader->readResponse($validationSubject);
  54. $storeId = $this->subjectReader->readStoreId($validationSubject);
  55. if (!empty($response['transactionResponse']['transHashSha2'])) {
  56. return $this->validateHash(
  57. $validationSubject,
  58. $this->config->getTransactionSignatureKey($storeId),
  59. 'transHashSha2',
  60. 'generateSha512Hash'
  61. );
  62. } elseif (!empty($response['transactionResponse']['transHash'])) {
  63. return $this->validateHash(
  64. $validationSubject,
  65. $this->config->getLegacyTransactionHash($storeId),
  66. 'transHash',
  67. 'generateMd5Hash'
  68. );
  69. }
  70. return $this->createResult(
  71. false,
  72. [
  73. __('The authenticity of the gateway response could not be verified.')
  74. ],
  75. [self::ERROR_TRANSACTION_HASH]
  76. );
  77. }
  78. /**
  79. * Validates the response again the legacy MD5 spec
  80. *
  81. * @param array $validationSubject
  82. * @param string $storedHash
  83. * @param string $hashField
  84. * @param string $generateFunction
  85. * @return ResultInterface
  86. */
  87. private function validateHash(
  88. array $validationSubject,
  89. string $storedHash,
  90. string $hashField,
  91. string $generateFunction
  92. ): ResultInterface {
  93. $storeId = $this->subjectReader->readStoreId($validationSubject);
  94. $response = $this->subjectReader->readResponse($validationSubject);
  95. $transactionResponse = $response['transactionResponse'];
  96. /*
  97. * Authorize.net is inconsistent with how they hash and heuristically trying to detect whether or not they used
  98. * the amount to calculate the hash is risky because their responses are incorrect in some cases.
  99. * Refund uses the amount when referencing a transaction but will use 0 when refunding without a reference.
  100. * Non-refund reference transactions such as (void/capture) don't use the amount. Authorize/auth&capture
  101. * transactions will use amount but if there is an AVS error the response will indicate the transaction was a
  102. * reference transaction so this can't be heuristically detected by looking at combinations of refTransID
  103. * and transId (yes they also mixed the letter casing for "id"). Their documentation doesn't talk about this
  104. * and to make this even better, none of their official SDKs support the new hash field to compare
  105. * implementations. Therefore the only way to safely validate this hash without failing for even more
  106. * unexpected corner cases we simply need to validate with and without the amount.
  107. */
  108. try {
  109. $amount = $this->subjectReader->readAmount($validationSubject);
  110. } catch (\InvalidArgumentException $e) {
  111. $amount = 0;
  112. }
  113. $hash = $this->{$generateFunction}(
  114. $storedHash,
  115. $this->config->getLoginId($storeId),
  116. sprintf('%.2F', $amount),
  117. $transactionResponse['transId'] ?? ''
  118. );
  119. $valid = Security::compareStrings($hash, $transactionResponse[$hashField]);
  120. if (!$valid && $amount > 0) {
  121. $hash = $this->{$generateFunction}(
  122. $storedHash,
  123. $this->config->getLoginId($storeId),
  124. '0.00',
  125. $transactionResponse['transId'] ?? ''
  126. );
  127. $valid = Security::compareStrings($hash, $transactionResponse[$hashField]);
  128. }
  129. if ($valid) {
  130. return $this->createResult(true);
  131. }
  132. return $this->createResult(
  133. false,
  134. [
  135. __('The authenticity of the gateway response could not be verified.')
  136. ],
  137. [self::ERROR_TRANSACTION_HASH]
  138. );
  139. }
  140. /**
  141. * Generates a Md5 hash to compare against AuthNet's.
  142. *
  143. * @param string $merchantMd5
  144. * @param string $merchantApiLogin
  145. * @param string $amount
  146. * @param string $transactionId
  147. * @return string
  148. * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
  149. */
  150. private function generateMd5Hash(
  151. $merchantMd5,
  152. $merchantApiLogin,
  153. $amount,
  154. $transactionId
  155. ) {
  156. return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount));
  157. }
  158. /**
  159. * Generates a SHA-512 hash to compare against AuthNet's.
  160. *
  161. * @param string $merchantKey
  162. * @param string $merchantApiLogin
  163. * @param string $amount
  164. * @param string $transactionId
  165. * @return string
  166. * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
  167. */
  168. private function generateSha512Hash(
  169. $merchantKey,
  170. $merchantApiLogin,
  171. $amount,
  172. $transactionId
  173. ) {
  174. $message = '^' . $merchantApiLogin . '^' . $transactionId . '^' . $amount . '^';
  175. return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantKey)));
  176. }
  177. }