SimplePath.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <?php
  2. /**
  3. * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License").
  6. * You may not use this file except in compliance with the License.
  7. * A copy of the License is located at
  8. *
  9. * http://aws.amazon.com/apache2.0
  10. *
  11. * or in the "license" file accompanying this file. This file is distributed
  12. * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
  13. * express or implied. See the License for the specific language governing
  14. * permissions and limitations under the License.
  15. */
  16. namespace Amazon\Core\Model\Config;
  17. use Amazon\Core\Helper\Data as CoreHelper;
  18. use Amazon\Core\Model\AmazonConfig;
  19. use Magento\Framework\App\State;
  20. use Magento\Framework\App\Cache\Type\Config as CacheTypeConfig;
  21. use Magento\Backend\Model\UrlInterface;
  22. use Magento\Payment\Helper\Formatter;
  23. use \phpseclib\Crypt\RSA;
  24. use \phpseclib\Crypt\AES;
  25. /**
  26. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  27. */
  28. class SimplePath
  29. {
  30. const CONFIG_XML_PATH_PRIVATE_KEY = 'payment/amazon_payments/simplepath/privatekey';
  31. const CONFIG_XML_PATH_PUBLIC_KEY = 'payment/amazon_payments/simplepath/publickey';
  32. private $_spIds = [
  33. 'USD' => 'AUGT0HMCLQVX1',
  34. 'GBP' => 'A1BJXVS5F6XP',
  35. 'EUR' => 'A2ZAYEJU54T1BM',
  36. 'JPY' => 'A1MCJZEB1HY93J',
  37. ];
  38. private $_mapCurrencyRegion = [
  39. 'EUR' => 'de',
  40. 'USD' => 'us',
  41. 'GBP' => 'uk',
  42. 'JPY' => 'ja',
  43. ];
  44. /**
  45. * @var
  46. */
  47. private $_storeId;
  48. /**
  49. * @var
  50. */
  51. private $_websiteId;
  52. /**
  53. * @var string
  54. */
  55. private $_scope;
  56. /**
  57. * @var int
  58. */
  59. private $_scopeId;
  60. /**
  61. * @var CoreHelper
  62. */
  63. private $coreHelper;
  64. /**
  65. * @var AmazonConfig
  66. */
  67. private $amazonConfig;
  68. /**
  69. * SimplePath constructor.
  70. * @param CoreHelper $coreHelper
  71. * @param AmazonConfig $amazonConfig
  72. * @param \Magento\Framework\App\Config\ConfigResource\ConfigInterface $config
  73. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  74. * @param \Magento\Framework\App\ProductMetadataInterface $productMeta
  75. * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor
  76. * @param \Magento\Framework\Message\ManagerInterface $messageManager
  77. * @param \Magento\Framework\App\ResourceConnection $connection
  78. * @param \Magento\Framework\App\Cache\Manager $cacheManager
  79. * @param \Magento\Framework\App\Request\Http $request
  80. * @param State $state
  81. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  82. * @param UrlInterface $backendUrl
  83. * @param \Magento\Paypal\Model\Config $paypal
  84. * @param \Psr\Log\LoggerInterface $logger
  85. * @throws \Magento\Framework\Exception\LocalizedException
  86. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  87. */
  88. public function __construct(
  89. CoreHelper $coreHelper,
  90. AmazonConfig $amazonConfig,
  91. \Magento\Framework\App\Config\ConfigResource\ConfigInterface $config,
  92. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  93. \Magento\Framework\App\ProductMetadataInterface $productMeta,
  94. \Magento\Framework\Encryption\EncryptorInterface $encryptor,
  95. \Magento\Framework\Message\ManagerInterface $messageManager,
  96. \Magento\Framework\App\ResourceConnection $connection,
  97. \Magento\Framework\App\Cache\Manager $cacheManager,
  98. \Magento\Framework\App\Request\Http $request,
  99. \Magento\Framework\App\State $state,
  100. \Magento\Store\Model\StoreManagerInterface $storeManager,
  101. \Magento\Backend\Model\UrlInterface $backendUrl,
  102. \Magento\Paypal\Model\Config $paypal,
  103. \Psr\Log\LoggerInterface $logger
  104. ) {
  105. $this->coreHelper = $coreHelper;
  106. $this->amazonConfig = $amazonConfig;
  107. $this->config = $config;
  108. $this->scopeConfig = $scopeConfig;
  109. $this->productMeta = $productMeta;
  110. $this->encryptor = $encryptor;
  111. $this->backendUrl = $backendUrl;
  112. $this->cacheManager = $cacheManager;
  113. $this->connection = $connection;
  114. $this->state = $state;
  115. $this->request = $request;
  116. $this->storeManager = $storeManager;
  117. $this->paypal = $paypal;
  118. $this->logger = $logger;
  119. $this->messageManager = $messageManager;
  120. // Find store ID and scope
  121. $this->_websiteId = $request->getParam('website', 0);
  122. $this->_storeId = $request->getParam('store', 0);
  123. $this->_scope = $request->getParam('scope');
  124. // Website scope
  125. if ($this->_websiteId) {
  126. $this->_scope = !$this->_scope ? 'websites' : $this->_scope;
  127. } else {
  128. $this->_websiteId = $storeManager->getWebsite()->getId();
  129. }
  130. // Store scope
  131. if ($this->_storeId) {
  132. $this->_websiteId = $this->storeManager->getStore($this->_storeId)->getWebsite()->getId();
  133. $this->_scope = !$this->_scope ? 'stores' : $this->_scope;
  134. } else {
  135. $this->_storeId = $storeManager->getWebsite($this->_websiteId)->getDefaultStore()->getId();
  136. }
  137. // Set scope ID
  138. switch ($this->_scope) {
  139. case 'websites':
  140. $this->_scopeId = $this->_websiteId;
  141. break;
  142. case 'stores':
  143. $this->_scopeId = $this->_storeId;
  144. break;
  145. default:
  146. $this->_scope = 'default';
  147. $this->_scopeId = 0;
  148. break;
  149. }
  150. }
  151. /**
  152. * Return domain
  153. */
  154. private function getEndpointDomain()
  155. {
  156. return in_array($this->getConfig('currency/options/default'), ['EUR', 'GBP'])
  157. ? 'https://payments-eu.amazon.com/'
  158. : 'https://payments.amazon.com/';
  159. }
  160. /**
  161. * Return register popup endpoint URL
  162. */
  163. public function getEndpointRegister()
  164. {
  165. return $this->getEndpointDomain() . 'register';
  166. }
  167. /**
  168. * Return pubkey endpoint URL
  169. */
  170. public function getEndpointPubkey()
  171. {
  172. return $this->getEndpointDomain() . 'register/getpublickey';
  173. }
  174. /**
  175. * Generate and save RSA keys
  176. */
  177. public function generateKeys()
  178. {
  179. $rsa = new RSA();
  180. $keys = $rsa->createKey(2048);
  181. $encrypt = $this->encryptor->encrypt($keys['privatekey']);
  182. $this->config
  183. ->saveConfig(self::CONFIG_XML_PATH_PUBLIC_KEY, $keys['publickey'], 'default', 0)
  184. ->saveConfig(self::CONFIG_XML_PATH_PRIVATE_KEY, $encrypt, 'default', 0);
  185. $this->cacheManager->clean([CacheTypeConfig::TYPE_IDENTIFIER]);
  186. return $keys;
  187. }
  188. /**
  189. * Delete key-pair from config
  190. */
  191. public function destroyKeys()
  192. {
  193. $this->config
  194. ->deleteConfig(self::CONFIG_XML_PATH_PUBLIC_KEY, 'default', 0)
  195. ->deleteConfig(self::CONFIG_XML_PATH_PRIVATE_KEY, 'default', 0);
  196. $this->cacheManager->clean([CacheTypeConfig::TYPE_IDENTIFIER]);
  197. }
  198. /**
  199. * Return RSA public key
  200. *
  201. * @param bool $pemformat
  202. * @param bool $reset
  203. * @return mixed|string|string[]|null
  204. */
  205. public function getPublicKey($pemformat = false, $reset = false)
  206. {
  207. $publickey = $this->scopeConfig->getValue(self::CONFIG_XML_PATH_PUBLIC_KEY, 'default', 0);
  208. // Generate key pair
  209. if (!$publickey || $reset || strlen($publickey) < 300) {
  210. $keys = $this->generateKeys();
  211. $publickey = $keys['publickey'];
  212. }
  213. if (!$pemformat) {
  214. $pubtrim = ['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n"];
  215. $publickey = str_replace($pubtrim, ['','',''], $publickey);
  216. // Remove binary characters
  217. $publickey = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $publickey);
  218. }
  219. return $publickey;
  220. }
  221. /**
  222. * Return RSA private key
  223. */
  224. public function getPrivateKey()
  225. {
  226. return $this->encryptor->decrypt($this->scopeConfig->getValue(self::CONFIG_XML_PATH_PRIVATE_KEY, 'default', 0));
  227. }
  228. /**
  229. * Convert key to PEM format for openssl functions
  230. */
  231. public function key2pem($key)
  232. {
  233. return "-----BEGIN PUBLIC KEY-----\n" .
  234. chunk_split($key, 64, "\n") .
  235. "-----END PUBLIC KEY-----\n";
  236. }
  237. /**
  238. * Verify and decrypt JSON payload
  239. *
  240. * @param string $payloadJson
  241. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  242. */
  243. public function decryptPayload($payloadJson, $autoEnable = true, $autoSave = true)
  244. {
  245. try {
  246. $payload = (object) json_decode($payloadJson);
  247. $payloadVerify = clone $payload;
  248. // Unencrypted via admin
  249. if ($this->state->getAreaCode() == 'adminhtml' &&
  250. isset($payload->merchant_id, $payload->access_key, $payload->secret_key)
  251. ) {
  252. return $this->saveToConfig($payloadJson, $autoEnable);
  253. }
  254. // Validate JSON
  255. if (!isset($payload->encryptedKey, $payload->encryptedPayload, $payload->iv, $payload->sigKeyID)) {
  256. throw new \Magento\Framework\Validator\Exception(
  257. __(
  258. 'Unable to import Amazon keys. ' .
  259. 'Please verify your JSON format and values.'
  260. )
  261. );
  262. }
  263. foreach ($payload as $key => $value) {
  264. $payload->$key = rawurldecode($value);
  265. }
  266. // Retrieve Amazon public key to verify signature
  267. try {
  268. $client = new \Zend_Http_Client(
  269. $this->getEndpointPubkey(),
  270. [
  271. 'maxredirects' => 2,
  272. 'timeout' => 30,
  273. ]
  274. );
  275. $client->setParameterGet(['sigkey_id' => $payload->sigKeyID]);
  276. $response = $client->request();
  277. $amazonPublickey = urldecode($response->getBody());
  278. } catch (\Exception $e) {
  279. throw new \Magento\Framework\Validator\Exception(__($e->getMessage()));
  280. }
  281. // Use raw JSON (without signature or URL decode) as the data to verify signature
  282. unset($payloadVerify->signature);
  283. $payloadVerifyJson = json_encode($payloadVerify);
  284. // Verify signature using Amazon publickey and JSON paylaod
  285. if ($amazonPublickey &&
  286. openssl_verify(
  287. $payloadVerifyJson,
  288. base64_decode($payload->signature),
  289. $this->key2pem($amazonPublickey),
  290. 'SHA256'
  291. )
  292. ) {
  293. // Decrypt Amazon key using own private key
  294. $decryptedKey = null;
  295. openssl_private_decrypt(
  296. base64_decode($payload->encryptedKey),
  297. $decryptedKey,
  298. $this->getPrivateKey(),
  299. OPENSSL_PKCS1_OAEP_PADDING
  300. );
  301. // Decrypt final payload (AES 256-bit CBC)
  302. $aes = new AES();
  303. $aes->setKey($decryptedKey);
  304. $aes->setIV(base64_decode($payload->iv, true));
  305. $aes->setKeyLength(256);
  306. $finalPayload = $aes->decrypt(base64_decode($payload->encryptedPayload));
  307. // Remove binary characters
  308. $finalPayload = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $finalPayload);
  309. if (json_decode($finalPayload)) {
  310. if ($autoSave) {
  311. $this->saveToConfig($finalPayload, $autoEnable);
  312. $this->destroyKeys();
  313. }
  314. return $finalPayload;
  315. }
  316. } else {
  317. throw new \Magento\Framework\Validator\Exception("Unable to verify signature for JSON payload.");
  318. }
  319. } catch (\Exception $e) {
  320. $this->logger->critical($e);
  321. $this->messageManager->addError(__($e->getMessage()));
  322. $link = 'https://payments.amazon.com/help/202024240';
  323. $this->messageManager->addError(
  324. __(
  325. "If you're experiencing consistent errors with transferring keys, " .
  326. "click <a href=\"%1\" target=\"_blank\">Manual Transfer Instructions</a> to learn more.",
  327. $link
  328. )
  329. );
  330. }
  331. return false;
  332. }
  333. /**
  334. * Save values to Mage config
  335. *
  336. * @param $json
  337. * @param bool $autoEnable
  338. * @return bool
  339. */
  340. public function saveToConfig($json, $autoEnable = true)
  341. {
  342. if ($values = (object) json_decode($json)) {
  343. foreach ($values as $key => $value) {
  344. $values->{strtolower($key)} = $value;
  345. }
  346. $this->config->saveConfig(
  347. 'payment/amazon_payment/merchant_id',
  348. $values->merchant_id,
  349. $this->_scope,
  350. $this->_scopeId
  351. );
  352. $this->config->saveConfig(
  353. 'payment/amazon_payment/client_id',
  354. $values->client_id,
  355. $this->_scope,
  356. $this->_scopeId
  357. );
  358. $this->config->saveConfig(
  359. 'payment/amazon_payment/client_secret',
  360. $this->encryptor->encrypt($values->client_secret),
  361. $this->_scope,
  362. $this->_scopeId
  363. );
  364. $this->config->saveConfig(
  365. 'payment/amazon_payment/access_key',
  366. $values->access_key,
  367. $this->_scope,
  368. $this->_scopeId
  369. );
  370. $this->config->saveConfig(
  371. 'payment/amazon_payment/secret_key',
  372. $this->encryptor->encrypt($values->secret_key),
  373. $this->_scope,
  374. $this->_scopeId
  375. );
  376. $currency = $this->getConfig('currency/options/default');
  377. if (isset($this->_mapCurrencyRegion[$currency])) {
  378. $this->config->saveConfig(
  379. 'payment/amazon_payment/payment_region',
  380. $this->_mapCurrencyRegion[$currency],
  381. $this->_scope,
  382. $this->_scopeId
  383. );
  384. }
  385. if ($autoEnable) {
  386. $this->autoEnable();
  387. }
  388. $this->cacheManager->clean([CacheTypeConfig::TYPE_IDENTIFIER]);
  389. return true;
  390. }
  391. }
  392. /**
  393. * Auto-enable payment method
  394. */
  395. public function autoEnable()
  396. {
  397. if (!$this->getConfig('payment/amazon_payment/active')) {
  398. $this->config->saveConfig('payment/amazon_payment/active', true, $this->_scope, $this->_scopeId);
  399. $this->messageManager->addSuccessMessage(__("Login and Pay with Amazon is now enabled."));
  400. }
  401. }
  402. /**
  403. * Return listener URL
  404. */
  405. public function getReturnUrl()
  406. {
  407. $baseUrl = $this->storeManager->getStore($this->_storeId)->getBaseUrl(UrlInterface::URL_TYPE_WEB, true);
  408. $baseUrl = str_replace('http:', 'https:', $baseUrl);
  409. $params = 'website=' . $this->_websiteId . '&store=' . $this->_storeId . '&scope=' . $this->_scope;
  410. return $baseUrl . 'amazon_core/simplepath/listener?' . urlencode($params);
  411. }
  412. /**
  413. * Return array of form POST params for SimplePath sign up
  414. */
  415. public function getFormParams()
  416. {
  417. // Get redirect URLs and store URL-s
  418. $urlArray = [];
  419. $baseUrls = [];
  420. $stores = $this->storeManager->getStores();
  421. foreach ($stores as $store) {
  422. // Get secure base URL
  423. if ($baseUrl = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB, true)) {
  424. $value = $baseUrl . 'amazon/login/processAuthHash/';
  425. $urlArray[] = $value;
  426. $url = parse_url($baseUrl);
  427. if (isset($url['host'])) {
  428. $baseUrls[] = 'https://' . $url['host'];
  429. }
  430. }
  431. // Get unsecure base URL
  432. if ($baseUrl = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB, false)) {
  433. $url = parse_url($baseUrl);
  434. if (isset($url['host'])) {
  435. $baseUrls[] = 'https://' . $url['host'];
  436. }
  437. }
  438. }
  439. $urlArray = array_unique($urlArray);
  440. $baseUrls = array_unique($baseUrls);
  441. $coreVersion = $this->coreHelper->getVersion();
  442. if (!$coreVersion) {
  443. $coreVersion = '--';
  444. }
  445. $currency = $this->getConfig('currency/options/default');
  446. return [
  447. 'keyShareURL' => $this->getReturnUrl(),
  448. 'publicKey' => $this->getPublicKey(),
  449. 'locale' => $this->getConfig('general/locale/code'),
  450. 'source' => 'SPPL',
  451. 'spId' => isset($this->_spIds[$currency]) ? $this->_spIds[$currency] : '',
  452. 'spSoftwareVersion' => $this->productMeta->getVersion(),
  453. 'spAmazonPluginVersion' => $coreVersion,
  454. 'merchantStoreDescription' => $this->getConfig('general/store_information/name'),
  455. 'merchantLoginDomains[]' => $baseUrls,
  456. 'merchantLoginRedirectURLs[]' => $urlArray,
  457. ];
  458. }
  459. /**
  460. * Return config value based on scope and scope ID
  461. */
  462. public function getConfig($path)
  463. {
  464. return $this->scopeConfig->getValue($path, $this->_scope, $this->_scopeId);
  465. }
  466. /**
  467. * Return payment region based on currency
  468. */
  469. public function getRegion()
  470. {
  471. $currency = $this->getConfig('currency/options/default');
  472. $region = isset($this->_mapCurrencyRegion[$currency]) ? strtoupper($this->_mapCurrencyRegion[$currency]) : '';
  473. if ($region == 'DE') {
  474. $region = 'Euro Region';
  475. }
  476. return $region ? $region : 'US';
  477. }
  478. /**
  479. * Return a valid store currency, otherwise return null
  480. */
  481. public function getCurrency()
  482. {
  483. $currency = $this->getConfig('currency/options/default');
  484. return (isset($this->_mapCurrencyRegion[$currency])) ? $currency : null;
  485. }
  486. /**
  487. * Return merchant country
  488. */
  489. public function getCountry()
  490. {
  491. $co = $this->getConfig('paypal/general/merchant_country');
  492. return $co ? $co : 'US';
  493. }
  494. /**
  495. * Return array of config for JSON AmazonSp variable.
  496. */
  497. public function getJsonAmazonSpConfig()
  498. {
  499. return [
  500. 'co' => $this->getCountry(),
  501. 'region' => $this->getRegion(),
  502. 'currency' => $this->getCurrency(),
  503. 'amazonUrl' => $this->getEndpointRegister(),
  504. 'pollUrl' => $this->backendUrl->getUrl('amazonsp/simplepath/poll/'),
  505. 'isSecure' => (int) ($this->request->isSecure()),
  506. 'hasOpenssl' => (int) (extension_loaded('openssl')),
  507. 'formParams' => $this->getFormParams(),
  508. 'isMultiCurrencyRegion' => (int) $this->amazonConfig->isMulticurrencyRegion(),
  509. ];
  510. }
  511. }