Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PLUG-144: Encrypt private key #44

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
39 changes: 34 additions & 5 deletions .github/workflows/e2e-tests.yml

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions Block/Adminhtml/System/Config/Field/Base64FileUpload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
/**
* Copyright © TrueLayer Ltd. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace TrueLayer\Connect\Block\Adminhtml\System\Config\Field;

use Magento\Framework\Data\Form\Element\AbstractElement;
use Magento\Config\Block\System\Config\Form\Field;
use TrueLayer\Connect\Model\Config\Source\Mode;

/**
* Color picker for admin config field
*/
class Base64FileUpload extends Field
{
protected $_template = 'TrueLayer_Connect::system/config/button/base64-file-upload.phtml';

// public function
/**
* @param AbstractElement $element
* @return string
*/
protected function _getElementHtml(AbstractElement $element)
{
$htmlTextInputId = $element->getHtmlId();
$mode = $element->getData('field_config')['depends']['fields']['mode']['value'] ?? Mode::SANDBOX;
$fieldType = $element->getData('field_config')['type'] ?? 'text';
$displayValue = $element->getValue();
$disabled = $element->getDisabled();
if ($displayValue && $fieldType == 'obscure') {
$displayValue = '******';
}

$this->setData([
'htmlTextInputId' => $htmlTextInputId,
'mode' => $mode,
'fieldType' => $fieldType,
'displayValue' => $displayValue,
'disabled' => $disabled,
]);
return $this->_toHtml();
}
}
37 changes: 13 additions & 24 deletions Controller/Adminhtml/Credentials/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 5 additions & 9 deletions Model/Config/System/ConnectionRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
];
Expand All @@ -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 '';
}

/**
Expand Down
180 changes: 18 additions & 162 deletions Model/System/Config/Backend/PrivateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
2 changes: 1 addition & 1 deletion Service/Client/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading