SodiumChachaPatch.php 4.93 KB
<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
declare(strict_types=1);

namespace Magento\EncryptionKey\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\App\ObjectManager;

/**
 * Migrate encrypted configuration values to the latest cipher
 */
class SodiumChachaPatch implements DataPatchInterface
{
    /**
     * @var \Magento\Framework\Config\ScopeInterface
     */
    private $scope;

    /**
     * @var \Magento\Framework\Setup\ModuleDataSetupInterface
     */
    private $moduleDataSetup;

    /**
     * @var \Magento\Config\Model\Config\Structure
     */
    private $structure;

    /**
     * @var \Magento\Framework\Encryption\EncryptorInterface
     */
    private $encryptor;

    /**
     * @var \Magento\Framework\App\State
     */
    private $state;

    /**
     * SodiumChachaPatch constructor.
     * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup
     * @param \Magento\Config\Model\Config\Structure\Proxy $structure
     * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor
     * @param \Magento\Framework\App\State $state
     * @param \Magento\Framework\Config\ScopeInterface|null $scope
     */
    public function __construct(
        \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup,
        \Magento\Config\Model\Config\Structure\Proxy $structure,
        \Magento\Framework\Encryption\EncryptorInterface $encryptor,
        \Magento\Framework\App\State $state,
        \Magento\Framework\Config\ScopeInterface $scope = null
    ) {
        $this->moduleDataSetup = $moduleDataSetup;
        $this->structure = $structure;
        $this->encryptor = $encryptor;
        $this->state = $state;
        $this->scope = $scope ?? ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class);
    }

    /**
     * @inheritdoc
     */
    public function apply()
    {
        $this->moduleDataSetup->startSetup();

        $this->reEncryptSystemConfigurationValues();

        $this->moduleDataSetup->endSetup();
    }

    /**
     * @inheritdoc
     */
    public static function getDependencies()
    {
        return [];
    }

    /**
     * @inheritdoc
     */
    public function getAliases()
    {
        return [];
    }

    /**
     * Re encrypt sensitive data in the system configuration
     */
    private function reEncryptSystemConfigurationValues()
    {
        $table = $this->moduleDataSetup->getTable('core_config_data');
        $hasEncryptedData = $this->moduleDataSetup->getConnection()->fetchOne(
            $this->moduleDataSetup->getConnection()
                ->select()
                ->from($table, [new \Zend_Db_Expr('count(value)')])
                ->where('value LIKE ?', '0:2%')
        );
        if ($hasEncryptedData !== '0') {
            $currentScope = $this->scope->getCurrentScope();
            $structure = $this->structure;
            $paths = $this->state->emulateAreaCode(
                \Magento\Framework\App\Area::AREA_ADMINHTML,
                function () use ($structure) {
                    $this->scope->setCurrentScope(\Magento\Framework\App\Area::AREA_ADMINHTML);
                    /** Returns list of structure paths to be re encrypted */
                    $paths = $structure->getFieldPathsByAttribute(
                        'backend_model',
                        \Magento\Config\Model\Config\Backend\Encrypted::class
                    );
                    /** Returns list of mapping between configPath => [structurePaths] */
                    $mappedPaths = $structure->getFieldPaths();
                    foreach ($mappedPaths as $mappedPath => $data) {
                        foreach ($data as $structurePath) {
                            if ($structurePath !== $mappedPath && $key = array_search($structurePath, $paths)) {
                                $paths[$key] = $mappedPath;
                            }
                        }
                    }

                    return array_unique($paths);
                }
            );
            $this->scope->setCurrentScope($currentScope);
            // walk through found data and re-encrypt it
            if ($paths) {
                $values = $this->moduleDataSetup->getConnection()->fetchPairs(
                    $this->moduleDataSetup->getConnection()
                        ->select()
                        ->from($table, ['config_id', 'value'])
                        ->where('path IN (?)', $paths)
                        ->where('value NOT LIKE ?', '')
                );
                foreach ($values as $configId => $value) {
                    $this->moduleDataSetup->getConnection()->update(
                        $table,
                        ['value' => $this->encryptor->encrypt($this->encryptor->decrypt($value))],
                        ['config_id = ?' => (int)$configId]
                    );
                }
            }
        }
    }
}