diff --git a/Block/Adminhtml/System/Config/Field/Base64FileUpload.php b/Block/Adminhtml/System/Config/Field/Base64FileUpload.php
new file mode 100644
index 0000000..f65b795
--- /dev/null
+++ b/Block/Adminhtml/System/Config/Field/Base64FileUpload.php
@@ -0,0 +1,47 @@
+getHtmlId();
+ $mode = $element->getData('field_config')['depends']['fields']['mode']['value'] ?? Mode::SANDBOX;
+ $fieldType = $element->getData('field_config')['type'] ?? 'text';
+ $tooltip = $element->getTooltip();
+ $element->setTooltip();
+ $displayValue = $element->getValue();
+ if ($displayValue && $fieldType == 'obscure') {
+ $displayValue = '******';
+ }
+
+ $this->setData([
+ 'htmlTextInputId' => $htmlTextInputId,
+ 'mode' => $mode,
+ 'fieldType' => $fieldType,
+ 'displayValue' => $displayValue,
+ 'tooltip' => $tooltip,
+ ]);
+ return $this->_toHtml();
+ }
+}
diff --git a/Controller/Adminhtml/Credentials/Check.php b/Controller/Adminhtml/Credentials/Check.php
index f7cd6bc..2341d1e 100644
--- a/Controller/Adminhtml/Credentials/Check.php
+++ b/Controller/Adminhtml/Credentials/Check.php
@@ -154,52 +154,41 @@ private function getCredentials(): array
$clientId = $this->getRequest()->getParam('sandbox_client_id');
$clientSecret = $this->getRequest()->getParam('sandbox_client_secret');
$keyId = $this->getRequest()->getParam('sandbox_key_id');
+ $privateKey = $this->getRequest()->getParam('sandbox_private_key');
} else {
$clientId = $this->getRequest()->getParam('production_client_id');
$clientSecret = $this->getRequest()->getParam('production_client_secret');
$keyId = $this->getRequest()->getParam('production_key_id');
+ $privateKey = $this->getRequest()->getParam('production_private_key');
}
$configCredentials = $this->configProvider->getCredentials($storeId, $mode === Mode::SANDBOX);
if ($clientSecret == '******') {
$clientSecret = $configCredentials['client_secret'];
}
+ if ($privateKey == '******') {
+ $privateKey = $configCredentials['private_key'];
+ } else {
+ if ($privateKey) {
+ $decoded = base64_decode($privateKey, true);
+ if (@base64_encode($decoded) === $privateKey) {
+ $privateKey = $decoded;
+ }
+ }
+ }
return [
'store_id' => $storeId,
'credentials' => [
'client_id' => $clientId,
'client_secret' => $clientSecret,
- 'private_key' => $this->getPrivateKeyPath($configCredentials),
+ 'private_key' => $privateKey,
'key_id' => $keyId,
'cache_encryption_key' => $configCredentials['cache_encryption_key']
]
];
}
- /**
- * @param array $configCredentials
- * @return string
- * @throws FileSystemException
- */
- private function getPrivateKeyPath(array $configCredentials): string
- {
- if ($privateKey = $this->getRequest()->getParam('private_key')) {
- $path = $this->directoryList->getPath('var') . self::PEM_UPLOAD_FILE;
- $fileInfo = $this->file->getPathInfo($path);
-
- if (!$this->file->fileExists($fileInfo['dirname'])) {
- $this->file->mkdir($fileInfo['dirname']);
- }
-
- $this->file->write($path, $privateKey);
-
- return $path;
- }
-
- return $configCredentials['private_key'];
- }
-
/**
* @return void
* @throws FileSystemException
diff --git a/Model/Config/System/ConnectionRepository.php b/Model/Config/System/ConnectionRepository.php
index b3459e3..62be0ec 100644
--- a/Model/Config/System/ConnectionRepository.php
+++ b/Model/Config/System/ConnectionRepository.php
@@ -41,7 +41,7 @@ public function getCredentials(?int $storeId = null, ?bool $forceSandbox = null)
return [
"client_id" => $this->getClientId($storeId, $isSandBox),
"client_secret" => $this->getClientSecret($storeId, $isSandBox),
- "private_key" => $this->getPathToPrivateKey($storeId, $isSandBox),
+ "private_key" => $this->getPrivateKey($storeId, $isSandBox),
"key_id" => $this->getKeyId($storeId, $isSandBox),
"cache_encryption_key" => $this->getCacheEncryptionKey($storeId)
];
@@ -52,18 +52,14 @@ public function getCredentials(?int $storeId = null, ?bool $forceSandbox = null)
* @param bool $isSandBox
* @return string
*/
- private function getPathToPrivateKey(?int $storeId = null, bool $isSandBox = false): string
+ private function getPrivateKey(?int $storeId = null, bool $isSandBox = false): string
{
$path = $isSandBox ? self::XML_PATH_SANDBOX_PRIVATE_KEY : self::XML_PATH_PRODUCTION_PRIVATE_KEY;
- if (!$savedPrivateKey = $this->getStoreValue($path, $storeId)) {
- return '';
+ if ($value = $this->getStoreValue($path, $storeId)) {
+ return $this->encryptor->decrypt($value);
}
- try {
- return $this->directoryList->getPath('var') . '/truelayer/' . $savedPrivateKey;
- } catch (\Exception $exception) {
- return '';
- }
+ return '';
}
/**
diff --git a/Model/System/Config/Backend/PrivateKey.php b/Model/System/Config/Backend/PrivateKey.php
index d316b7a..27d95a8 100644
--- a/Model/System/Config/Backend/PrivateKey.php
+++ b/Model/System/Config/Backend/PrivateKey.php
@@ -7,178 +7,34 @@
namespace TrueLayer\Connect\Model\System\Config\Backend;
-use Magento\Framework\App\Cache\TypeListInterface;
-use Magento\Framework\App\Config\ScopeConfigInterface;
-use Magento\Framework\App\Config\Value;
-use Magento\Framework\Data\Collection\AbstractDb;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Filesystem;
-use Magento\Framework\Filesystem\Directory\ReadInterface;
-use Magento\Framework\Filesystem\Io\File;
-use Magento\Framework\Model\Context;
-use Magento\Framework\Model\ResourceModel\AbstractResource;
-use Magento\Framework\Registry;
-use TrueLayer\Connect\Api\Config\System\ConnectionInterface;
+use Magento\Config\Model\Config\Backend\Encrypted;
/**
- * Backend model for saving certificate file
+ * Backend model for saving certificate
*/
-class PrivateKey extends Value
+class PrivateKey extends Encrypted
{
- public const FILENAME = 'private-key.pem';
/**
- * @var File
- */
- private $file;
- /**
- * @var ReadInterface
- */
- private $tmpDirectory;
- /**
- * @var ReadInterface
- */
- private $varDirectory;
-
- /**
- * @param Context $context
- * @param Registry $registry
- * @param ScopeConfigInterface $config
- * @param TypeListInterface $cacheTypeList
- * @param Filesystem $filesystem
- * @param File $file
- * @param AbstractResource|null $resource
- * @param AbstractDb|null $resourceCollection
- * @param array $data
- */
- public function __construct(
- Context $context,
- Registry $registry,
- ScopeConfigInterface $config,
- TypeListInterface $cacheTypeList,
- Filesystem $filesystem,
- File $file,
- AbstractResource $resource = null,
- AbstractDb $resourceCollection = null,
- array $data = []
- ) {
- $this->file = $file;
- $this->tmpDirectory = $filesystem->getDirectoryRead('sys_tmp');
- $this->varDirectory = $filesystem->getDirectoryRead('var');
- parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
- }
-
- /**
- * Process additional data before save config.
+ * Decode and encrypt value before saving
*
- * @return $this
- * @throws LocalizedException
- */
- public function beforeSave(): self
- {
- $value = (array)$this->getValue();
- $sandbox = $this->getPath() === ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY;
- $directory = $this->getDirectory($sandbox);
-
- if (!empty($value['delete'])) {
- $this->deleteCertificateAndReset($this->isObjectNew() ? '' : $this->getOldValue());
- return $this;
- }
-
- $tmpName = $this->getTmpName($sandbox);
- $isUploading = (is_string($tmpName) && !empty($tmpName) && $this->tmpDirectory->isExist($tmpName));
-
- if (!$isUploading) {
- $this->setValue($this->isObjectNew() ? '' : $this->getOldValue());
- return $this;
- }
-
- if ($isUploading) {
- $tmpPath = $this->tmpDirectory->getAbsolutePath($tmpName);
- if (!$this->tmpDirectory->stat($tmpPath)['size']) {
- throw new LocalizedException(__('The TrueLayer certificate file is empty.'));
- }
-
- $destinationPath = $this->varDirectory->getAbsolutePath('truelayer/' . $directory);
-
- $filePath = $directory . self::FILENAME;
- $this->file->checkAndCreateFolder($destinationPath);
- $this->file->mv(
- $tmpPath,
- $this->varDirectory->getAbsolutePath('truelayer/' . $filePath)
- );
- $this->setValue($filePath);
- }
-
- return $this;
- }
-
- /**
- * Delete the cert file from disk when deleting the setting.
- *
- * @return $this
- */
- public function beforeDelete()
- {
- $returnValue = parent::beforeDelete();
- $filePath = $this->isObjectNew() ? '' : $this->getOldValue();
- if ($filePath) {
- $absolutePath = $this->varDirectory->getAbsolutePath('truelayer/' . $filePath);
- if ($this->file->fileExists($absolutePath)) {
- $this->file->rm($absolutePath);
- }
- }
- return $returnValue;
- }
-
- /**
- * Delete the cert file and unset the config value.
- *
- * @param string $filePath
* @return void
*/
- private function deleteCertificateAndReset(string $filePath): void
+ public function beforeSave()
{
- if (!empty($filePath)) {
- $absolutePath = $this->varDirectory->getAbsolutePath('truelayer/' . $filePath);
- if ($this->file->fileExists($absolutePath)) {
- $this->file->rm($absolutePath);
+ $this->_dataSaveAllowed = false;
+ $value = (string)$this->getValue();
+ // don't save value, if an obscured value was received. This indicates that data was not changed.
+ if (!preg_match('/^\*+$/', $value) && !empty($value)) {
+ $this->_dataSaveAllowed = true;
+ $decoded = base64_decode($value, true);
+ if (!$decoded || @base64_encode($decoded) !== $value) {
+ $decoded = '';
}
- }
-
- $this->setValue('');
- }
-
- /**
- * Returns the directory based on set scope.
- *
- * @param bool $sandbox
- * @return string
- */
- private function getDirectory(bool $sandbox): string
- {
- $mode = $sandbox ? 'sandbox' : 'production';
- return $this->getScope() !== 'default'
- ? sprintf('%s/%s/%s/', $mode, $this->getScope(), $this->getScopeId())
- : sprintf('%s/default/', $mode);
- }
-
- /**
- * Returns the path to the uploaded tmp_file based on set scope.
- *
- * @param bool $sandbox
- * @return string
- */
- private function getTmpName(bool $sandbox): ?string
- {
- $files = $_FILES;
- if (empty($files)) {
- return null;
- }
- try {
- $tmpName = $files['groups']['tmp_name']['general']['fields'][$sandbox ? 'sandbox_private_key' : 'production_private_key']['value'];
- return empty($tmpName) ? null : $tmpName;
- } catch (\Exception $e) {
- return null;
+ $encrypted = $decoded ? $this->_encryptor->encrypt($decoded) : null;
+ $this->setValue($encrypted);
+ } elseif (empty($value)) {
+ $this->setValue(null);
+ $this->_dataSaveAllowed = true;
}
}
}
diff --git a/Service/Client/ClientFactory.php b/Service/Client/ClientFactory.php
index 1fa6450..6a391af 100644
--- a/Service/Client/ClientFactory.php
+++ b/Service/Client/ClientFactory.php
@@ -74,7 +74,7 @@ private function createClient(array $credentials, ?bool $forceSandbox = null): ?
$clientFactory->clientId($credentials['client_id'])
->clientSecret($credentials['client_secret'])
->keyId($credentials['key_id'])
- ->pemFile($credentials['private_key'])
+ ->pem($credentials['private_key'])
->useProduction(is_null($forceSandbox) ? !$this->configProvider->isSandbox() : !$forceSandbox);
if ($cacheEncryptionKey) {
diff --git a/Setup/UpgradeData.php b/Setup/UpgradeData.php
new file mode 100644
index 0000000..c37fe9e
--- /dev/null
+++ b/Setup/UpgradeData.php
@@ -0,0 +1,69 @@
+getVersion();
+
+ if(version_compare($setupVersion, '1.0.0', '<=')) {
+ $this->encryptPrivateKeys();
+ }
+ }
+
+ private function encryptPrivateKeys()
+ {
+ $this->dataCollection->addFilter('path', ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY, 'or');
+ $this->dataCollection->addFilter('path', ConnectionInterface::XML_PATH_PRODUCTION_PRIVATE_KEY, 'or');
+ $this->dataCollection->loadWithFilter();
+ /** @var \Magento\Framework\App\Config\Value[] $configItems */
+ $configItems = $this->dataCollection->getItems();
+ $this->dataCollection->clear()->getSelect()->reset('where');
+
+ $configPaths = [ConnectionInterface::XML_PATH_PRODUCTION_PRIVATE_KEY,ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY];
+ $varDirectory = $this->filesystem->getDirectoryRead('var');
+ foreach ($configItems as $configItem) {
+ $configPath = $configItem->getPath();
+ if (!in_array($configPath, $configPaths)) {
+ continue;
+ }
+ $configValue = $configItem->getValue();
+ if (!$configValue) {
+ continue;
+ }
+ $isPath = str_starts_with($configValue, 'sandbox/') || str_starts_with($configValue, 'production/');
+ $absPath = $isPath ? $varDirectory->getAbsolutePath('truelayer/' . $configValue) : null;
+ $fileExists = $absPath ? $this->file->fileExists($absPath, true) : false;
+ if ($fileExists) {
+ $privateKey = $this->file->read($absPath, null);
+ $encryptedKey = $this->encryptor->encrypt($privateKey);
+ $configItem->setValue($encryptedKey);
+ $this->file->rm($absPath);
+ } else {
+ $configItem->setValue(null);
+ }
+ $this->dataResourceModel->save($configItem);
+ }
+ }
+}
diff --git a/etc/adminhtml/system/general.xml b/etc/adminhtml/system/general.xml
index d63387e..2e26e78 100644
--- a/etc/adminhtml/system/general.xml
+++ b/etc/adminhtml/system/general.xml
@@ -76,18 +76,22 @@
production
-
+
+
payment/truelayer/production_private_key
+ TrueLayer\Connect\Block\Adminhtml\System\Config\Field\Base64FileUpload
TrueLayer\Connect\Model\System\Config\Backend\PrivateKey
1
production
-
+
+
payment/truelayer/sandbox_private_key
+ TrueLayer\Connect\Block\Adminhtml\System\Config\Field\Base64FileUpload
TrueLayer\Connect\Model\System\Config\Backend\PrivateKey
1
diff --git a/etc/module.xml b/etc/module.xml
index 02fb4d7..a76b59b 100644
--- a/etc/module.xml
+++ b/etc/module.xml
@@ -7,7 +7,7 @@
-->
-
+
diff --git a/view/adminhtml/templates/system/config/button/base64-file-upload.phtml b/view/adminhtml/templates/system/config/button/base64-file-upload.phtml
new file mode 100644
index 0000000..56a4ecf
--- /dev/null
+++ b/view/adminhtml/templates/system/config/button/base64-file-upload.phtml
@@ -0,0 +1,55 @@
+getData('mode');
+$htmlClass = 'truelayer_upload-file-as-string_'.$mode;
+$scope = 'truelayer_private-key-upload_'.$mode;
+$fieldType = $block->getData('fieldType');
+$displayValue = $block->getData('displayValue');
+$tooltip = $block->getData('tooltip');
+
+$htmlTextInputId = $block->getData('htmlTextInputId');
+$htmlFileInputId = $htmlTextInputId.'_file';
+?>
+
+
+
+
diff --git a/view/adminhtml/templates/system/config/button/credentials.phtml b/view/adminhtml/templates/system/config/button/credentials.phtml
index 224257e..d5c97f6 100644
--- a/view/adminhtml/templates/system/config/button/credentials.phtml
+++ b/view/adminhtml/templates/system/config/button/credentials.phtml
@@ -22,16 +22,16 @@ use TrueLayer\Connect\Block\Adminhtml\System\Config\Button\Credentials;
document.querySelector('#truelayer_general').addEventListener('change', (e) => {
// Check mode
- if (e.target.getAttribute('name').includes('[mode]')) {
+ if (e.target.getAttribute('name')?.includes('[mode]')) {
truelayer_mode = e.target.value;
}
if (e.target.getAttribute('type') === 'file') {
const FR = new FileReader();
- FR.onload = () => {
- truelayer_mode === 'sandbox'
- ? private_key_sandbox = FR.result
+ FR.onload = () => {
+ truelayer_mode === 'sandbox'
+ ? private_key_sandbox = FR.result
: private_key_production = FR.result;
}
@@ -46,19 +46,20 @@ use TrueLayer\Connect\Block\Adminhtml\System\Config\Button\Credentials;
jQuery("input[name='groups[general][fields][production_client_id][value]']").val(),
"production_client_secret":
jQuery("input[name='groups[general][fields][production_client_secret][value]']").val(),
+ "production_private_key":
+ jQuery("input[name='groups[general][fields][production_private_key][value]']").val(),
"production_key_id":
jQuery("input[name='groups[general][fields][production_key_id][value]']").val(),
"sandbox_client_id":
jQuery("input[name='groups[general][fields][sandbox_client_id][value]']").val(),
"sandbox_client_secret":
jQuery("input[name='groups[general][fields][sandbox_client_secret][value]']").val(),
+ "sandbox_private_key":
+ jQuery("input[name='groups[general][fields][sandbox_private_key][value]']").val(),
"sandbox_key_id":
jQuery("input[name='groups[general][fields][sandbox_key_id][value]']").val(),
"mode":
jQuery("select[name='groups[general][fields][mode][value]']").val(),
- "private_key": truelayer_mode === 'sandbox' ? private_key_sandbox : private_key_production,
- "delete_private_key":
- jQuery("input[name='groups[general][fields][sandbox_private_key][value][delete]']").is(':checked'),
};
new Ajax.Request('= $block->escapeUrl($block->getApiCheckUrl()) ?>', {
diff --git a/view/adminhtml/web/js/handle-upload.js b/view/adminhtml/web/js/handle-upload.js
new file mode 100644
index 0000000..872628e
--- /dev/null
+++ b/view/adminhtml/web/js/handle-upload.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright © TrueLayer Ltd, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'uiComponent',
+ 'jquery'
+], function (Component, $) {
+ 'use strict';
+
+ return Component.extend({
+ clickUpload() {
+ document.getElementById(this.htmlFileInputId).click();
+ },
+ handleFile(object, event) {
+ const textInput = document.getElementById(this.htmlTextInputId);
+ // Get a reference to the file
+ const file = event?.target?.files[0];
+
+ if (!file) {
+ return;
+ }
+ // Encode the file using the FileReader API
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ // Use a regex to remove data url part
+ const base64String = reader.result
+ .replace(/^data:.+,/, "");
+
+ textInput.value = base64String;
+ };
+ reader.readAsDataURL(file);
+ }
+ });
+});