Skip to content

Commit 38bc7f1

Browse files
AC-12767: Implement extensible data re-encryption mechanism
1 parent 15a78a0 commit 38bc7f1

File tree

19 files changed

+1524
-2
lines changed

19 files changed

+1524
-2
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Config\Model\Data\ReEncryptorList\CoreConfigDataReEncryptor;
9+
10+
use Magento\Framework\App\Config\Initial;
11+
use Magento\Framework\App\ResourceConnection;
12+
use Magento\Config\Model\Config\Backend\Encrypted;
13+
use Magento\Framework\Encryption\EncryptorInterface;
14+
use Magento\EncryptionKey\Model\Data\ReEncryptorList\ReEncryptor\HandlerInterface;
15+
use Magento\EncryptionKey\Model\Data\ReEncryptorList\ReEncryptor\Handler\ErrorFactory;
16+
17+
/**
18+
* Handler for core configuration re-encryption.
19+
*/
20+
class Handler implements HandlerInterface
21+
{
22+
/**
23+
* @var string
24+
*/
25+
private const TABLE_NAME = "core_config_data";
26+
27+
/**
28+
* @var string
29+
*/
30+
private const BACKEND_MODEL = Encrypted::class;
31+
32+
/**
33+
* @var Initial
34+
*/
35+
private Initial $config;
36+
37+
/**
38+
* @var EncryptorInterface
39+
*/
40+
private EncryptorInterface $encryptor;
41+
42+
/**
43+
* @var ResourceConnection
44+
*/
45+
private ResourceConnection $resourceConnection;
46+
47+
/**
48+
* @var ErrorFactory
49+
*/
50+
private ErrorFactory $errorFactory;
51+
52+
/**
53+
* @param Initial $config
54+
* @param EncryptorInterface $encryptor
55+
* @param ResourceConnection $resourceConnection
56+
* @param ErrorFactory $errorFactory
57+
*/
58+
public function __construct(
59+
Initial $config,
60+
EncryptorInterface $encryptor,
61+
ResourceConnection $resourceConnection,
62+
ErrorFactory $errorFactory
63+
) {
64+
$this->config = $config;
65+
$this->encryptor = $encryptor;
66+
$this->resourceConnection = $resourceConnection;
67+
$this->errorFactory = $errorFactory;
68+
}
69+
70+
/**
71+
* @inheritDoc
72+
*/
73+
public function reEncrypt(): array
74+
{
75+
$paths = [];
76+
$errors = [];
77+
78+
foreach ($this->config->getMetadata() as $path => $processor) {
79+
if (isset($processor['backendModel']) &&
80+
$processor['backendModel'] === self::BACKEND_MODEL
81+
) {
82+
$paths[] = $path;
83+
}
84+
}
85+
86+
if ($paths) {
87+
$tableName = $this->resourceConnection->getTableName(
88+
self::TABLE_NAME
89+
);
90+
91+
$connection = $this->resourceConnection->getConnection();
92+
93+
$select = $connection->select()
94+
->from($tableName, ['config_id', 'value'])
95+
->where('path IN (?)', $paths)
96+
->where('value != ?', '')
97+
->where('value IS NOT NULL');
98+
99+
foreach ($connection->fetchPairs($select) as $configId => $value) {
100+
try {
101+
$connection->update(
102+
$tableName,
103+
['value' => $this->encryptor->encrypt($this->encryptor->decrypt($value))],
104+
['config_id = ?' => (int) $configId]
105+
);
106+
} catch (\Throwable $e) {
107+
$errors[] = $this->errorFactory->create(
108+
"config_id",
109+
$configId,
110+
$e->getMessage()
111+
);
112+
113+
continue;
114+
}
115+
}
116+
}
117+
118+
return $errors;
119+
}
120+
}

app/code/Magento/Config/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"magento/module-directory": "*",
1414
"magento/module-email": "*",
1515
"magento/module-media-storage": "*",
16-
"magento/module-store": "*"
16+
"magento/module-store": "*",
17+
"magento/module-encryption-key": "*"
1718
},
1819
"type": "magento2-module",
1920
"license": [

app/code/Magento/Config/etc/di.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,17 @@
409409
</argument>
410410
</arguments>
411411
</type>
412+
<virtualType name="Magento\Config\Model\Data\ReEncryptorList\CoreConfigDataReEncryptor" type="Magento\EncryptionKey\Model\Data\ReEncryptorList\ReEncryptor">
413+
<arguments>
414+
<argument name="description" xsi:type="string">Re-encrypts 'value' column in the 'core_config_data' DB table.</argument>
415+
<argument name="handler" xsi:type="object">Magento\Config\Model\Data\ReEncryptorList\CoreConfigDataReEncryptor\Handler</argument>
416+
</arguments>
417+
</virtualType>
418+
<type name="Magento\EncryptionKey\Model\Data\ReEncryptorList">
419+
<arguments>
420+
<argument name="reEncryptors" xsi:type="array">
421+
<item name="core_config_data" xsi:type="object">Magento\Config\Model\Data\ReEncryptorList\CoreConfigDataReEncryptor</item>
422+
</argument>
423+
</arguments>
424+
</type>
412425
</config>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\EncryptionKey\Console\Command;
9+
10+
use Magento\Framework\Console\Cli;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Magento\EncryptionKey\Model\Data\ReEncryptorList;
15+
16+
/**
17+
* Command for displaying a list of available data re-encryptors.
18+
*/
19+
class ListReEncryptorsCommand extends Command
20+
{
21+
/**
22+
* @var ReEncryptorList
23+
*/
24+
private ReEncryptorList $reEncryptorList;
25+
26+
/**
27+
* @param ReEncryptorList $reEncryptorList
28+
*/
29+
public function __construct(
30+
ReEncryptorList $reEncryptorList
31+
) {
32+
$this->reEncryptorList = $reEncryptorList;
33+
34+
parent::__construct();
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
protected function configure()
41+
{
42+
$this->setName('encryption:data:list-re-encryptors');
43+
44+
$this->setDescription(
45+
'Shows a list of available data re-encryptors.'
46+
);
47+
48+
parent::configure();
49+
}
50+
51+
/**
52+
* @inheritDoc
53+
*/
54+
protected function execute(InputInterface $input, OutputInterface $output)
55+
{
56+
foreach ($this->reEncryptorList->getReEncryptors() as $name => $reEncryptor) {
57+
$output->writeln(
58+
sprintf(
59+
'<fg=green>%-40s</> %s',
60+
$name,
61+
$reEncryptor->getDescription()
62+
)
63+
);
64+
}
65+
66+
return Cli::RETURN_SUCCESS;
67+
}
68+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\EncryptionKey\Console\Command;
9+
10+
use DateInterval;
11+
use Magento\Framework\Console\Cli;
12+
use Symfony\Component\Console\Command\Command;
13+
use Symfony\Component\Console\Input\InputArgument;
14+
use Symfony\Component\Console\Input\InputInterface;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
use Magento\EncryptionKey\Model\Data\ReEncryptorList;
17+
18+
/**
19+
* Command for re-encryption of encrypted data using current encryption key.
20+
*/
21+
class ReEncryptDataCommand extends Command
22+
{
23+
/**
24+
* @var string
25+
*/
26+
private const INPUT_KEY_ENCRYPTORS = 'encryptors';
27+
28+
/**
29+
* @var ReEncryptorList
30+
*/
31+
private ReEncryptorList $reEncryptorList;
32+
33+
/**
34+
* @param ReEncryptorList $reEncryptorList
35+
*/
36+
public function __construct(
37+
ReEncryptorList $reEncryptorList
38+
) {
39+
$this->reEncryptorList = $reEncryptorList;
40+
41+
parent::__construct();
42+
}
43+
44+
/**
45+
* @inheritDoc
46+
*/
47+
protected function configure()
48+
{
49+
$this->setName('encryption:data:re-encrypt');
50+
51+
$this->setDescription(
52+
'Re-encrypts encrypted data using current encryption key.'
53+
);
54+
55+
$this->addArgument(
56+
self::INPUT_KEY_ENCRYPTORS,
57+
InputArgument::IS_ARRAY,
58+
'Space-separated list of re-encryptors to use.'
59+
);
60+
61+
parent::configure();
62+
}
63+
64+
/**
65+
* @inheritDoc
66+
*/
67+
protected function execute(InputInterface $input, OutputInterface $output)
68+
{
69+
$requestedReEncryptorsNames = $input->getArgument(
70+
self::INPUT_KEY_ENCRYPTORS
71+
);
72+
73+
$availableReEncryptors = $this->reEncryptorList->getReEncryptors();
74+
75+
if (empty($requestedReEncryptorsNames)) {
76+
$requestedReEncryptorsNames = array_keys($availableReEncryptors);
77+
}
78+
79+
foreach ($requestedReEncryptorsNames as $name) {
80+
if (!isset($availableReEncryptors[$name])) {
81+
$output->writeLn(
82+
sprintf("<fg=red>Re-encryptor '%s' could not be found!</>", $name)
83+
);
84+
85+
continue;
86+
}
87+
88+
$reEncryptor = $availableReEncryptors[$name];
89+
90+
$output->writeLn(
91+
sprintf("Executing '%s' re-encryptor...", $name)
92+
);
93+
94+
try {
95+
$startTime = new \DateTimeImmutable();
96+
97+
$errors = $reEncryptor->reEncrypt();
98+
99+
$endTime = new \DateTimeImmutable();
100+
101+
$elapsedTime = $this->formatInterval(
102+
$startTime->diff($endTime)
103+
);
104+
} catch (\Throwable $e) {
105+
$output->writeLn("<fg=red>Failed due to the following error:</>");
106+
107+
$output->writeLn(
108+
sprintf("<fg=white;bg=red>%s</>", $e->getMessage())
109+
);
110+
111+
continue;
112+
}
113+
114+
if (empty($errors)) {
115+
$output->writeLn(
116+
sprintf(
117+
"<fg=green>Done successfully in %s.</>",
118+
$elapsedTime
119+
)
120+
);
121+
} else {
122+
$output->writeLn(
123+
sprintf(
124+
"<fg=yellow>Done in %s but with the following errors:</>",
125+
$elapsedTime
126+
)
127+
);
128+
129+
foreach ($errors as $error) {
130+
$output->writeLn(
131+
sprintf(
132+
"<fg=black;bg=yellow>[%s %s]: %s</>",
133+
$error->getRowIdField(),
134+
$error->getRowIdValue(),
135+
$error->getMessage()
136+
)
137+
);
138+
}
139+
}
140+
}
141+
142+
return Cli::RETURN_SUCCESS;
143+
}
144+
145+
/**
146+
* Formats a date interval.
147+
*
148+
* @param DateInterval $interval
149+
*
150+
* @return string
151+
*/
152+
private function formatInterval(DateInterval $interval): string
153+
{
154+
$days = (int) $interval->format('%d');
155+
156+
$hours = $days * 24 + (int) $interval->format('%H');
157+
$minutes = $interval->format('%I');
158+
$seconds = $interval->format('%S');
159+
160+
return sprintf("%s:%s:%s", $hours, $minutes, $seconds);
161+
}
162+
}

0 commit comments

Comments
 (0)