├── .circleci
└── config.yml
├── Console
├── GenerateEncryptionKey.php
├── GetReEncryptedCloudEnvironmentsKeys.php
├── InvalidateOldEncryptionKeys.php
├── ReencryptColumn.php
├── ReencryptTfaData.php
└── ReencryptUnhandledCoreConfigData.php
├── LICENSE
├── Model
├── DeploymentConfig.php
├── EncodingHelper.php
├── ReEncryptCloudEnvKeysCommand.php
└── RecursiveDataProcessor.php
├── Plugin
└── LogDecrypts.php
├── README.md
├── Service
├── ChangeEncryptionKey.php
├── InvalidatedKeyHasher.php
└── ReencryptEnvSystemConfigurationValues.php
├── composer.json
├── dev
├── README.md
└── test.sh
├── etc
├── adminhtml
│ └── system.xml
├── di.xml
└── module.xml
└── registration.php
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | build:
5 | machine:
6 | image: default
7 | steps:
8 | - checkout
9 | - run:
10 | name: Verify Architecture
11 | command: uname -m
12 | - run:
13 | name: Log docker version
14 | command: |
15 | docker --version && docker compose version
16 | - run:
17 | name: Set up magento and run tests
18 | command: |
19 | INSTALL_LOC=$(pwd)
20 | cd /tmp/
21 | git clone https://github.com/AmpersandHQ/magento-docker-test-instance --branch 0.1.21
22 | cd magento-docker-test-instance
23 | CURRENT_EXTENSION="$INSTALL_LOC" FULL_INSTALL=1 ./bin/mtest-make 2-4-6-p3
24 | ./bin/mtest 'cp vendor/gene/module-encryption-key-manager/dev/test.sh .'
25 | ./bin/mtest 'chmod +x ./test.sh'
26 | ./bin/mtest './test.sh'
27 |
28 | workflows:
29 | version: 2
30 | build_and_test:
31 | jobs:
32 | - build
33 |
--------------------------------------------------------------------------------
/Console/GenerateEncryptionKey.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:generate');
75 | $this->setDescription('Generate a new encryption key');
76 | $this->setDefinition($options);
77 |
78 | parent::configure();
79 | }
80 |
81 | /**
82 | * @param InputInterface $input
83 | * @param OutputInterface $output
84 | * @return int
85 | */
86 | protected function execute(InputInterface $input, OutputInterface $output): int
87 | {
88 | $newKey = null;
89 | if ($input->getOption(self::INPUT_KEY_KEY)) {
90 | $newKey = $input->getOption(self::INPUT_KEY_KEY);
91 | $output->writeln('The provided crypt key will be used for re-encryption.');
92 | } else {
93 | $output->writeln('A new key will be generated for re-encryption, use "--key" to specify a custom key.');
94 | }
95 |
96 | if (!$input->getOption(self::INPUT_KEY_FORCE)) {
97 | $output->writeln('Run with --force to generate a new key. This will decrypt and reencrypt values in core_config_data and saved credit card info');
98 | return Cli::RETURN_FAILURE;
99 | }
100 |
101 | try {
102 | $countOfKeys = count(explode(PHP_EOL, $this->encryptor->exportKeys()));
103 | $output->writeln("The system currently has $countOfKeys keys");
104 |
105 | /**
106 | * This is heavily based on the below
107 | *
108 | * @see \Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key\Save::execute()
109 | */
110 | try {
111 | $this->state->setAreaCode('adminhtml');
112 | } catch (\Magento\Framework\Exception\LocalizedException $exception) {
113 | // Area code is already set
114 | }
115 | $this->emulation->startEnvironmentEmulation(0, 'adminhtml');
116 | $output->writeln('Generating a new encryption key using the magento core class');
117 | $this->changeEncryptionKey->setOutput($output);
118 | $this->changeEncryptionKey->setSkipSavedCreditCards(
119 | (bool)$input->getOption(self::INPUT_SKIP_SAVED_CREDIT_CARDS)
120 | );
121 | $this->changeEncryptionKey->changeEncryptionKey($newKey);
122 | $output->writeln('reEncryptEnvConfigurationValues - start');
123 | $this->reencryptEnvSystemConfigurationValues->execute();
124 | $output->writeln('reEncryptEnvConfigurationValues - end');
125 | $this->emulation->stopEnvironmentEmulation();
126 | $output->writeln('Cleaning cache');
127 |
128 | $value = $this->scopeConfig->getValue('gene/encryption_key_manager/invalidated_key_index');
129 | if ($value == null) {
130 | $this->configWriter->save(
131 | 'gene/encryption_key_manager/invalidated_key_index',
132 | (int) $countOfKeys - 1
133 | );
134 | }
135 |
136 | $this->cache->clean();
137 | $output->writeln('Done');
138 | } catch (\Throwable $throwable) {
139 | $output->writeln("" . $throwable->getMessage() . "");
140 | $output->writeln($throwable->getTraceAsString(), OutputInterface::VERBOSITY_VERBOSE);
141 | return Cli::RETURN_FAILURE;
142 | }
143 | return Cli::RETURN_SUCCESS;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Console/GetReEncryptedCloudEnvironmentsKeys.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:get-cloud-keys');
40 | $this->setDescription('Reencrypt cloud encrypted keys based on $_ENV variable. ' .
41 | 'The CLI command don\'t save new values. It has to be done manually.');
42 | $this->setDefinition([
43 | new InputOption(
44 | self::INPUT_KEY_SHOW_DECRYPTED,
45 | null,
46 | InputOption::VALUE_NONE,
47 | 'Whether to show decrypted values.'
48 | ),
49 | ]);
50 | parent::configure();
51 | }
52 |
53 | /**
54 | * Execute the command
55 | *
56 | * @param InputInterface $input
57 | * @param OutputInterface $output
58 | *
59 | * @return int
60 | */
61 | protected function execute(InputInterface $input, OutputInterface $output): int
62 | {
63 | $showDecrypted = !!$input->getOption(self::INPUT_KEY_SHOW_DECRYPTED);
64 |
65 | try {
66 | // get old encrypted, decrypted and new encrypted values
67 | $config = $this->reencryptCloudEnvKeysCommand->execute();
68 |
69 | if (!count($config)) {
70 | $output->writeln('There is no old encrypted environment variables found');
71 | return CLI::RETURN_SUCCESS;
72 | }
73 |
74 | $output->writeln("The CLI command doesn't rewrite values. " .
75 | "You have to update them manually in cloud console!");
76 | $output->writeln("Rows count: " . count($config) . "");
77 |
78 | foreach ($config as $name => $arr) {
79 | $output->writeln(str_pad('', 120, '#'));
80 |
81 | /** @var $arr array{value:string, newValue:string, decryptedValue:string} */
82 | $output->writeln("Name: {$name}");
83 | if ($showDecrypted) {
84 | $output->writeln("Dectypted value: {$arr['decryptedValue']}");
85 | }
86 | $output->writeln("Old Encrypted Value: {$arr['value']}");
87 | $output->writeln("New Encrypted Value: {$arr['newValue']}");
88 | }
89 |
90 | } catch (\Exception|\Throwable $e) {
91 | $this->logger->critical("Something went wrong while trying to reencrypt cloud variables.", [
92 | 'msg' => $e->getMessage(),
93 | 'trace' => $e->getTraceAsString(),
94 | ]);
95 | $output->writeln("" . $e->getMessage() . "");
96 |
97 | return CLI::RETURN_FAILURE;
98 | }
99 |
100 | return CLI::RETURN_SUCCESS;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Console/InvalidateOldEncryptionKeys.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:invalidate');
50 | $this->setDescription('Invalidate old encryption keys');
51 | $this->setDefinition($options);
52 |
53 | parent::configure();
54 | }
55 |
56 | /**
57 | * @param InputInterface $input
58 | * @param OutputInterface $output
59 | * @return int
60 | */
61 | protected function execute(InputInterface $input, OutputInterface $output): int
62 | {
63 | if (!$input->getOption(self::INPUT_KEY_FORCE)) {
64 | $output->writeln('Run with --force to invalidate old keys. You need to have thoroughly reviewed your entire site and database before doing this.');
65 | return Cli::RETURN_FAILURE;
66 | }
67 |
68 | try {
69 | /**
70 | * This is largely based on how the env.php is handled in
71 | * @see \Magento\EncryptionKey\Model\ResourceModel\Key\Change::changeEncryptionKey()
72 | */
73 | if (!$this->writer->checkIfWritable()) {
74 | throw new \Exception('Deployment configuration file is not writable.');
75 | }
76 |
77 | $keys = $this->deploymentConfig->get('crypt/key');
78 | $keys = preg_split('/\s+/s', trim((string)$keys));
79 | if (count($keys) <= 1) {
80 | throw new \Exception('Cannot invalidate when there is only one key');
81 | }
82 |
83 | $invalidatedKeys = $this->deploymentConfig->get('crypt/invalidated_key');
84 | $invalidatedKeys = array_filter(preg_split('/\s+/s', trim((string)$invalidatedKeys)));
85 |
86 | /**
87 | * All but the latest encryption key needs to be invalidated
88 | * - Wipe out the text so that its no longer a valid key
89 | * - keep a record of it for storing in 'crypt/invalidated_key'
90 | */
91 | $changes = false;
92 | $keySize = SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES;
93 | foreach ($keys as $id => $key) {
94 | if ($id === count($keys) - 1) {
95 | break; // last key needs to remain usable
96 | }
97 | if (str_starts_with($key, 'invalid')) {
98 | continue; // already been invalidated
99 | }
100 | $changes = true;
101 | $invalidatedKeys[] = $key; // this key needs to be added to the invalidated list
102 | $newKey = 'invalid' . $this->random->getRandomString($keySize - 7);
103 | $keys[$id] = $newKey;
104 | if (strlen($keys[$id]) !== $keySize) {
105 | throw new \Exception('Failed to invalidate the key with an appropriate length');
106 | }
107 | }
108 | unset($id, $key);
109 |
110 | if (!$changes) {
111 | $output->writeln('No further keys need invalidated');
112 | return Cli::RETURN_SUCCESS;
113 | }
114 |
115 | $output->writeln('Writing crypt/invalidated_key to env.php');
116 | $encryptInvalidSegment = new ConfigData(ConfigFilePool::APP_ENV);
117 | $encryptInvalidSegment->set('crypt/invalidated_key', implode(PHP_EOL, $invalidatedKeys));
118 | $this->writer->saveConfig([$encryptInvalidSegment->getFileKey() => $encryptInvalidSegment->getData()]);
119 |
120 | $output->writeln('Writing crypt/key to env.php');
121 | $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV);
122 | $encryptSegment->set('crypt/key', implode(PHP_EOL, $keys));
123 | $this->writer->saveConfig([$encryptSegment->getFileKey() => $encryptSegment->getData()]);
124 | $this->cache->clean();
125 | $output->writeln('Done');
126 | } catch (\Throwable $throwable) {
127 | $output->writeln("" . $throwable->getMessage() . "");
128 | $output->writeln($throwable->getTraceAsString(), OutputInterface::VERBOSITY_VERBOSE);
129 | return Cli::RETURN_FAILURE;
130 | }
131 | return Cli::RETURN_SUCCESS;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Console/ReencryptColumn.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:reencrypt-column');
75 | $this->setDescription('Re-encrypt a columns data with the latest key');
76 | $this->setDefinition($options);
77 |
78 | parent::configure();
79 | }
80 |
81 | /**
82 | * @param InputInterface $input
83 | * @param OutputInterface $output
84 | * @return int
85 | */
86 | protected function execute(InputInterface $input, OutputInterface $output): int
87 | {
88 | if (!$input->getOption(self::INPUT_KEY_FORCE)) {
89 | $output->writeln('Run with --force to make these changes, this will run in dry-run mode by default');
90 | }
91 |
92 | try {
93 | $keys = preg_split('/\s+/s', trim((string)$this->deploymentConfig->get('crypt/key')));
94 | $latestKeyNumber = count($keys) - 1;
95 | $output->writeln("The latest encryption key is number $latestKeyNumber, looking for old entries");
96 |
97 | $table = $input->getArgument(self::INPUT_KEY_TABLE);
98 | if (!strlen($table)) {
99 | throw new \Exception('Provide a table name');
100 | }
101 | if (in_array($table, ['core_config_data', 'tfa_user_config'])) {
102 | throw new \Exception('You cannot use this command for this table');
103 | }
104 | $identifier = $input->getArgument(self::INPUT_KEY_IDENTIFIER);
105 | if (!strlen($identifier)) {
106 | throw new \Exception('Provide an identifier');
107 | }
108 | $column = $input->getArgument(self::INPUT_KEY_COLUMN);
109 | if (!strlen($column)) {
110 | throw new \Exception('Provide an column');
111 | }
112 | $jsonField = null;
113 | if (strpos($column, '.') !== false) {
114 | list($column, $jsonField) = explode('.', $column);
115 | $output->writeln(
116 | "Looking for JSON field '$jsonField.$column' in '$table', identified by '$identifier'"
117 | );
118 | } else {
119 | $output->writeln("Looking for '$column' in '$table', identified by '$identifier'");
120 | }
121 | /**
122 | * @see \Magento\Framework\Model\ResourceModel\Db\AbstractDb::_getLoadSelect()
123 | */
124 | $tableName = $this->resourceConnection->getTableName($table);
125 | $connection = $this->resourceConnection->getConnection();
126 |
127 | if (!$connection->isTableExists($tableName)) {
128 | $output->writeln("The table {$tableName} doesn't exist");
129 | return Cli::RETURN_SUCCESS;
130 | }
131 |
132 | $field = $connection->quoteIdentifier(sprintf('%s.%s', $tableName, $column));
133 | $select = $connection->select()
134 | ->from($tableName, [$identifier, "$column"]);
135 | if ($jsonField === null) {
136 | $select = $select->where("($field LIKE '_:_:____%' OR $field LIKE '__:_:____%')")
137 | ->where("$field NOT LIKE ?", "$latestKeyNumber:_:__%");
138 | } else {
139 | $select = $select->where("($field LIKE '{%_:_:____%}' OR $field LIKE '{%__:_:____%}')");
140 | }
141 | $result = $connection->fetchAll($select);
142 | if (empty($result)) {
143 | $output->writeln('No old entries found');
144 | return Cli::RETURN_SUCCESS;
145 | }
146 | $connection->beginTransaction();
147 | $noResults = true;
148 | foreach ($result as $row) {
149 | $output->writeln(str_pad('', 120, '#'));
150 | $value = $row[$column];
151 | $fieldData = [];
152 | if ($jsonField !== null) {
153 | $fieldData = $this->jsonSerializer->unserialize($value);
154 | $value = $fieldData[$jsonField] ?? '';
155 | // Prevent re-processing fields & processing empty fields
156 | if (strpos($value, "$latestKeyNumber:") === 0 || $value === '') {
157 | continue;
158 | }
159 | }
160 | $noResults = false;
161 | $output->writeln("$identifier: {$row[$identifier]}");
162 | $output->writeln("ciphertext_old: " . $value);
163 | $valueDecrypted = $this->encryptor->decrypt($value);
164 | $output->writeln("plaintext: " . $valueDecrypted);
165 | $valueEncrypted = $this->encryptor->encrypt($valueDecrypted);
166 | $output->writeln("ciphertext_new: " . $valueEncrypted);
167 |
168 | if ($jsonField !== null) {
169 | $fieldData[$jsonField] = $valueEncrypted;
170 | $valueEncrypted = $this->jsonSerializer->serialize($fieldData);
171 | }
172 |
173 | if ($input->getOption(self::INPUT_KEY_FORCE)) {
174 | $connection->update(
175 | $tableName,
176 | [$column => $valueEncrypted],
177 | ["$identifier = ?" => $row[$identifier]]
178 | );
179 | } else {
180 | $output->writeln('Dry run mode, no changes have been made');
181 | }
182 | $output->writeln(str_pad('', 120, '#'));
183 | }
184 |
185 | if ($noResults) {
186 | $output->writeln('No old entries found');
187 | }
188 |
189 | $connection->commit();
190 | $this->cache->clean();
191 | $output->writeln('Done');
192 | } catch (\Throwable $throwable) {
193 | if ($this->resourceConnection->getConnection()->getTransactionLevel() > 0) {
194 | $this->resourceConnection->getConnection()->rollBack();
195 | }
196 | $output->writeln("" . $throwable->getMessage() . "");
197 | $output->writeln($throwable->getTraceAsString(), OutputInterface::VERBOSITY_VERBOSE);
198 | return Cli::RETURN_FAILURE;
199 | }
200 | return Cli::RETURN_SUCCESS;
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/Console/ReencryptTfaData.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:reencrypt-tfa-data');
54 | $this->setDescription('Re-encrypts tfa_user_config data with the latest key');
55 | $this->setDefinition($options);
56 |
57 | parent::configure();
58 | }
59 |
60 | /**
61 | * @param InputInterface $input
62 | * @param OutputInterface $output
63 | * @return int
64 | */
65 | protected function execute(InputInterface $input, OutputInterface $output): int
66 | {
67 | if (!$input->getOption(self::INPUT_KEY_FORCE)) {
68 | $output->writeln('Run with --force to make these changes, this will run in dry-run mode by default');
69 | $output->writeln('This CLI has only been tested with Google Authenticator (TOTP) and U2F (Yubikey, etc). If you use Authy or DUO you *MUST* verify before use.');
70 | }
71 |
72 | try {
73 | $keys = preg_split('/\s+/s', trim((string)$this->deploymentConfig->get('crypt/key')));
74 | $latestKeyNumber = count($keys) - 1;
75 | $output->writeln("The latest encryption key is number $latestKeyNumber, looking for old entries");
76 |
77 | $table = self::TFA_TABLE;
78 | $identifier = 'config_id';
79 | $column = 'encoded_config';
80 | $output->writeln("Looking for $column in $table, identified by '$identifier'");
81 |
82 | /**
83 | * @see \Magento\Framework\Model\ResourceModel\Db\AbstractDb::_getLoadSelect()
84 | */
85 | $tableName = $this->resourceConnection->getTableName($table);
86 | $connection = $this->resourceConnection->getConnection();
87 | if (!$connection->isTableExists($tableName)) {
88 | $output->writeln("The table {$tableName} doesn't exist");
89 | return Cli::RETURN_SUCCESS;
90 | }
91 | $field = $connection->quoteIdentifier(sprintf('%s.%s', $tableName, $column));
92 |
93 | $select = $connection->select()
94 | ->from($tableName, [$identifier, "$column"])
95 | ->where("($field LIKE '_:_:____%' OR $field LIKE '__:_:____%')")
96 | ->where("$field NOT LIKE ?", "$latestKeyNumber:_:__%");
97 |
98 | $result = $connection->fetchAll($select);
99 | if (empty($result)) {
100 | $output->writeln('No old entries found');
101 | return Cli::RETURN_SUCCESS;
102 | }
103 | $connection->beginTransaction();
104 | foreach ($result as $row) {
105 | $output->writeln(str_pad('', 120, '#'));
106 | $output->writeln("$identifier: {$row[$identifier]}");
107 | $value = $row[$column];
108 | $output->writeln("ciphertext_old: " . $value);
109 | $valueDecrypted = $this->encryptor->decrypt($value);
110 | if (empty($valueDecrypted)) {
111 | $output->writeln("plaintext_old: is null");
112 | continue;
113 | }
114 | $output->writeln("plaintext_old: " . $valueDecrypted);
115 | $valueDecrypted = json_decode($valueDecrypted);
116 |
117 | /**
118 | * Google Authenticator 2FA provider uses a nested encrypted value. So that we can also handle other
119 | * providers that may do the same, recursively process the originally decrypted value, re-encrypting
120 | * its children.
121 | */
122 | $valueDecrypted = $this->recursiveDataProcessor->down($valueDecrypted);
123 | $valueDecrypted = json_encode($valueDecrypted);
124 | $output->writeln("plaintext_new: " . $valueDecrypted);
125 | $valueEncrypted = $this->encryptor->encrypt($valueDecrypted);
126 | $output->writeln("ciphertext_new: " . $valueEncrypted);
127 |
128 | if ($input->getOption(self::INPUT_KEY_FORCE)) {
129 | $connection->update(
130 | $tableName,
131 | [$column => $valueEncrypted],
132 | ["$identifier = ?" => $row[$identifier]]
133 | );
134 | } else {
135 | $output->writeln('Dry run mode, no changes have been made');
136 | }
137 | $output->writeln(str_pad('', 120, '#'));
138 | }
139 | if ($this->recursiveDataProcessor->hasFailures()) {
140 | $output->writeln('We encountered some problems re-encrypting values in the tfa_user_config table which requires manual intervention');
141 | }
142 | $connection->commit();
143 | $this->cache->clean();
144 | $output->writeln('Done');
145 | } catch (\Throwable $throwable) {
146 | if ($this->resourceConnection->getConnection()->getTransactionLevel() > 0) {
147 | $this->resourceConnection->getConnection()->rollBack();
148 | }
149 | $output->writeln("" . $throwable->getMessage() . "");
150 | $output->writeln($throwable->getTraceAsString(), OutputInterface::VERBOSITY_VERBOSE);
151 | return Cli::RETURN_FAILURE;
152 | }
153 | return Cli::RETURN_SUCCESS;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/Console/ReencryptUnhandledCoreConfigData.php:
--------------------------------------------------------------------------------
1 | setName('gene:encryption-key-manager:reencrypt-unhandled-core-config-data');
49 | $this->setDescription('Re-encrypt unhandled core config data with the latest key');
50 | $this->setDefinition($options);
51 |
52 | parent::configure();
53 | }
54 |
55 | /**
56 | * @param InputInterface $input
57 | * @param OutputInterface $output
58 | * @return int
59 | */
60 | protected function execute(InputInterface $input, OutputInterface $output): int
61 | {
62 | if (!$input->getOption(self::INPUT_KEY_FORCE)) {
63 | $output->writeln('Run with --force to make these changes, this will run in dry-run mode by default');
64 | }
65 |
66 | try {
67 | $keys = preg_split('/\s+/s', trim((string)$this->deploymentConfig->get('crypt/key')));
68 | $latestKeyNumber = count($keys) - 1;
69 |
70 | $output->writeln("The latest encryption key is number $latestKeyNumber, looking for old entries");
71 |
72 | /*
73 | * Get all values which look like an encrypted value, that are not for the latest key
74 | */
75 | $ccdTable = $this->resourceConnection->getTableName('core_config_data');
76 | $connection = $this->resourceConnection->getConnection();
77 |
78 | if (!$connection->isTableExists($ccdTable)) {
79 | $output->writeln("The table {$ccdTable} doesn't exist");
80 | return Cli::RETURN_SUCCESS;
81 | }
82 |
83 | $select = $connection->select()
84 | ->from($ccdTable, ['*'])
85 | ->where('(value LIKE "_:_:____%" OR value LIKE "__:_:____%")')
86 | ->where('value NOT LIKE ?', "$latestKeyNumber:_:__%")
87 | ->where('value NOT LIKE ?', "a:%")
88 | ->where('value NOT LIKE ?', "s:%");
89 |
90 | $result = $connection->fetchAll($select);
91 | if (empty($result)) {
92 | $output->writeln('No old entries found');
93 | return Cli::RETURN_SUCCESS;
94 | }
95 | foreach ($result as $row) {
96 | $output->writeln(str_pad('', 120, '#'));
97 | $output->writeln("config_id: {$row['config_id']}");
98 | foreach ($row as $field => $value) {
99 | if (in_array($field, ['value', 'config_id'])) {
100 | continue;
101 | }
102 | $output->writeln("$field: $value");
103 | }
104 | $value = $row['value'];
105 | $output->writeln("ciphertext_old: " . $value);
106 | $valueDecrypted = $this->encryptor->decrypt($value);
107 | $output->writeln("plaintext: " . $valueDecrypted);
108 | $valueEncrypted = $this->encryptor->encrypt($valueDecrypted);
109 | $output->writeln("ciphertext_new: " . $valueEncrypted);
110 |
111 | if ($input->getOption(self::INPUT_KEY_FORCE)) {
112 | $connection->update(
113 | $ccdTable,
114 | ['value' => $valueEncrypted],
115 | ['config_id = ?' => (int)$row['config_id']]
116 | );
117 | } else {
118 | $output->writeln('Dry run mode, no changes have been made');
119 | }
120 | $output->writeln(str_pad('', 120, '#'));
121 | }
122 |
123 | $this->cache->clean();
124 | $output->writeln('Done');
125 | } catch (\Throwable $throwable) {
126 | $output->writeln("" . $throwable->getMessage() . "");
127 | $output->writeln($throwable->getTraceAsString(), OutputInterface::VERBOSITY_VERBOSE);
128 | return Cli::RETURN_FAILURE;
129 | }
130 | return Cli::RETURN_SUCCESS;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/Model/DeploymentConfig.php:
--------------------------------------------------------------------------------
1 | deploymentConfig->get('crypt/key')));
31 | } catch (\Exception) {
32 | return 0;
33 | }
34 | return count($keys) -1;
35 | }
36 |
37 | /**
38 | * Validate whether the value looks like digit:digit:string
39 | *
40 | * @param string $value
41 | * @return bool
42 | */
43 | public function isEncryptedValue(string $value): bool
44 | {
45 | preg_match('/^\d:\d:\S+/', $value, $matches);
46 | return !!count($matches);
47 | }
48 |
49 | /**
50 | * Returns whether the value is already encrypted
51 | *
52 | * @param string $encryptedValue
53 | * @return bool
54 | */
55 | public function isAlreadyUpdated(string $encryptedValue): bool
56 | {
57 | return str_starts_with($encryptedValue, $this->getLatestKeyNumber() . ":");
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Model/ReEncryptCloudEnvKeysCommand.php:
--------------------------------------------------------------------------------
1 | placeholder = $placeholderFactory->create(PlaceholderFactory::TYPE_ENVIRONMENT);
33 | }
34 |
35 | /**
36 | * Execute the command
37 | *
38 | * @param array|null $environmentVariables
39 | * @return array
40 | * @throws \Exception
41 | */
42 | public function execute(?array $environmentVariables = null): array
43 | {
44 | if ($environmentVariables === null) {
45 | if (!isset($_ENV)) {
46 | throw new \Exception("No environment variables defined");
47 | }
48 | $environmentVariables = $_ENV;
49 | }
50 |
51 | $config = [];
52 |
53 | foreach ($environmentVariables as $template => $value) {
54 | if (!$this->placeholder->isApplicable($template)
55 | || !$this->helper->isEncryptedValue($value)
56 | || $this->helper->isAlreadyUpdated($value)) {
57 | continue;
58 | }
59 |
60 | $decryptedValue = $this->encryptor->decrypt($value);
61 | $newValue = $this->encryptor->encrypt($decryptedValue);
62 | $config[$template] = compact('value', 'newValue', 'decryptedValue');
63 | }
64 |
65 | return $config;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Model/RecursiveDataProcessor.php:
--------------------------------------------------------------------------------
1 | $value) {
27 | if (is_array($value) || is_object($value)) {
28 | // If either array or object go down a level and process it and its children
29 | $value = $this->down($value);
30 | } else {
31 | // Only need to process encrypted strings
32 | if (is_string($value)) {
33 | // Previous iteration may have $matches so unset
34 | unset($matches);
35 | // Match on string that look encrypted (digit:digit:non_whitespace(1 or more)
36 | preg_match('/\d:\d:\S+/', $value, $matches);
37 | // If match found $matches[0] will exist
38 | if (isset($matches[0])) {
39 | // Re-encrypt value
40 | $decryptedValue = $this->encryptor->decrypt($value);
41 | if ($decryptedValue === '') {
42 | /**
43 | * \Magento\Framework\Encryption\Encryptor::decrypt seems to return '' on failure
44 | */
45 | $this->failures++;
46 | } else {
47 | $value = $this->encryptor->encrypt($decryptedValue);
48 | }
49 | // Remove $decryptedValue for future iterations
50 | unset($decryptedValue);
51 | }
52 | }
53 | }
54 | // Set value back to parent
55 | $this->setValue($layer, $key, $value);
56 | }
57 | return $layer;
58 | }
59 |
60 | /**
61 | * Wrapper to set value back to parent as element write syntax differs by type
62 | *
63 | * @param mixed $parent
64 | * @param mixed $key
65 | * @param mixed $value
66 | * @return mixed
67 | */
68 | private function setValue($parent, $key, $value)
69 | {
70 | if (is_array($parent)) {
71 | $parent[$key] = $value;
72 | } else {
73 | // $parent is object
74 | $parent->$key = $value;
75 | }
76 | return $parent;
77 | }
78 |
79 | /**
80 | * Did we encounter any cases were we were unable to decrypt & re-encrypt stored values
81 | * @return bool
82 | */
83 | public function hasFailures(): bool
84 | {
85 | return $this->failures > 0;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Plugin/LogDecrypts.php:
--------------------------------------------------------------------------------
1 | keyCount = count(explode(PHP_EOL, $encryptor->exportKeys())) - 1;
35 |
36 | // These need to come from deployment config because it is triggered so early in the request flow
37 | $this->enabled = (bool)$deploymentConfig->get(
38 | 'system/default/dev/debug/gene_encryption_manager_enable_decrypt_logging',
39 | false
40 | );
41 | $this->onlyLogOldKeyDecryptions = (bool)$deploymentConfig->get(
42 | 'system/default/dev/debug/gene_encryption_manager_only_log_old_decrypts',
43 | false
44 | );
45 | }
46 |
47 | /**
48 | * Log the source of a decryption, so that we can verify all keys are properly rotated
49 | *
50 | * @param Encryptor $subject
51 | * @param $result
52 | * @param $data
53 | * @return mixed
54 | */
55 | public function afterDecrypt(Encryptor $subject, $result, $data)
56 | {
57 | if (!$this->enabled) {
58 | return $result;
59 | }
60 | try {
61 | if (!(is_string($data) && strlen($data) > 5)) {
62 | // Not a string matching '0:0:X' or longer
63 | return $result;
64 | }
65 | if ($this->onlyLogOldKeyDecryptions && str_starts_with($data, $this->keyCount . ':')) {
66 | // We are decrypting a value identified by the current maximum key, no need to log
67 | return $result;
68 | }
69 |
70 | // don't logging values don't like as an encrypted value
71 | if (!$this->encodingHelper->isEncryptedValue($data)) {
72 | return $result;
73 | }
74 |
75 | $exception = new \Exception();
76 |
77 | /**
78 | * Generate a trace identifier based on the stack trace excluding any args
79 | */
80 | $traceIdentifier = [];
81 | foreach ($exception->getTrace() as $trace) {
82 | unset($trace['args']);
83 | $traceIdentifier[] = implode($trace);
84 | }
85 | $traceIdentifier = md5(implode($traceIdentifier));
86 |
87 | /**
88 | * This is a bit odd looking but it puts the entire trace in one line, many log management systems do not
89 | * like multi line logs and this can make it a bit easier to trace / filter in those systems
90 | *
91 | * Make the log entry single pipe separated line
92 | * Remove full path from trace for easier reading
93 | * BP defined at app/autoload.php
94 | */
95 | $traceString = str_replace(PHP_EOL, '|', $exception->getTraceAsString());
96 | $traceString = '|' . str_replace(BP . '/', '', $traceString);
97 |
98 | $this->logger->info(
99 | 'gene encryption manager - legacy decryption',
100 | [
101 | 'trace_id' => $traceIdentifier,
102 | 'trace' => $traceString
103 | ]
104 | );
105 | } catch (\Throwable $throwable) {
106 | $this->logger->error(
107 | 'gene encryption manager - error logging',
108 | [
109 | 'message' => $throwable->getMessage(),
110 | ]
111 | );
112 | }
113 | return $result;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # module-encryption-key-manager
2 |
3 | [](https://circleci.com/gh/genecommerce/module-encryption-key-manager)
4 |
5 | This module was built to aid with https://sansec.io/research/cosmicsting-hitting-major-stores
6 |
7 | From the sansec post
8 | > Upgrading is Insufficient
9 | > As we warned in our earlier article, it is crucial for merchants to upgrade or apply the official isolated fix. At this stage however, just patching for the CosmicSting vulnerability is likely to be insufficient.
10 | >
11 | >The stolen encryption key still allows attackers to generate web tokens even after upgrading. Merchants that are currently still vulnerable should consider their encryption key as compromised. Adobe offers functionality out of the box to change the encryption key while also re-encrypting existing secrets.
12 | >
13 | >Important note: generating a new encryption key using this functionality does not invalidate the old key. We recommend manually updating the old key in app/etc/env.php to a new value rather than removing it.
14 |
15 | Even with your store secured, there is the chance that a JWT was issued and may still be valid. Merchants are strongly encouraged to rotate their encryption key to be safe, and the Magento process of generating a new encryption key does not actually invalidate the old one.
16 |
17 | This module is provided as-is without any warranty. Test this on your local instances, then staging, then production. Use at your own risk.
18 |
19 | This module **does not conflict** with the [new hotfix](https://experienceleague.adobe.com/en/docs/commerce-knowledge-base/kb/troubleshooting/known-issues-patches-attached/security-update-available-for-adobe-commerce-apsb24-40-revised-to-include-isolated-patch-for-cve-2024-34102?#hotfix) released by Adobe. Both this module and that hotfix improve security in the same way, by making `SecretBasedJwksFactory` use the most recent key. This module also provides additional tooling and improvements, please read below.
20 |
21 | # Installation
22 | ```
23 | composer require gene/module-encryption-key-manager
24 | bin/magento setup:upgrade
25 | ```
26 |
27 | # How to Rotate your key and protect your store
28 |
29 | This is a rough list of steps that should be followed to prevent attacks with CosmicSting. Please read all of the steps carefully to understand the features this module provides, as well as the points of risk.
30 |
31 | ## Generate a new Key and prevent old ones from being used for JWT
32 |
33 | This should be every merchant's **priority!** Install this module and generate a new key with:
34 |
35 | `php bin/magento gene:encryption-key-manager:generate [--key=MY_32_CHAR_CRYPT_KEY] [--skip-saved-credit-cards]`
36 |
37 | This will force the JWT factory to use the newly generated key. Other areas of the application may continue to use the old keys. This step is the absolute priority and will help prevent attacks with CosmicSting.
38 |
39 | - Use the `--key` option to manually define the new key to use during re-encryption. If no custom key is provided, a new key will be generated.
40 | - Use the `--skip-saved-credit-cards` flag to skip re-encrypting the `sales_order_payment` `cc_number_enc` data. This table can be very large, and many stores will have no data saved in this column.
41 |
42 | ## Fully rotate your old keys
43 |
44 | You can take your time to do the following. You are safe from cosmicsting provided you have installed the [isolated patches](https://experienceleague.adobe.com/en/docs/commerce-knowledge-base/kb/troubleshooting/known-issues-patches-attached/security-update-available-for-adobe-commerce-apsb24-40-revised-to-include-isolated-patch-for-cve-2024-34102#isolated-patch-details) and used this module to generate a new encryption key.
45 |
46 | Then you are free to decide if you wish to re-encrypt your old data, and then invalidate your old key.
47 |
48 | 1. **Review your database** (make sure zgrep is on version 1.12) for any tables with encrypted values. Make sure your dump is `--human-readable` (magerun) or `--extended-insert=FALSE` (mysqldump) so that all values are on the same line as the `INSERT INTO`
49 | ```bash
50 | $ zgrep -P "VALUES\s*\(.*\d:\d:...*'" database.sql | awk '{print $3}' | uniq
51 | admin_user
52 | core_config_data
53 | customer_entity
54 | oauth_token
55 | oauth_consumer
56 | tfa_user_config
57 | admin_adobe_ims_webapi
58 | adobe_user_profile
59 | ```
60 |
61 | Or to get a overview of all found tables with amount of records:
62 | ```bash
63 | zgrep -P "VALUES\s*\(.*\d:\d:...*'" database.sql | awk '{print $3}' | sort | uniq -c
64 | ```
65 |
66 | 2. Review your `env.php`, if you store any encrypted values there they will need to be reissued by the provider as they may have been leaked.
67 | 2. **Review functions** using `->hash(` from the encryptor class. Changing the keys will result in a different hash.
68 | 3. If you have **custom logic** to handle that, it will be something you need to work that out manually.
69 | 3. **Generate a new key** `php bin/magento gene:encryption-key-manager:generate`
70 | 1. You can specify the new crypt key to use with `php bin/magento gene:encryption-key-manager:generate --key=MY_32_CHAR_CRYPT_KEY`
71 | 2. `Magento\Catalog\Model\View\Asset\Image` will continue to use the key at the `0` index
72 | 3. `Magento\JwtUserToken\Model\SecretBasedJwksFactory` will only use the most recently generated key at the highest index
73 | 4. **Fix missing config values** `php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data`
74 | 1. Re-run to verify `php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data`
75 | 4. **Fix 2FA data** `php bin/magento gene:encryption-key-manager:reencrypt-tfa-data`
76 | 1. Re-run to verify `php bin/magento gene:encryption-key-manager:reencrypt-tfa-data`
77 | 5. Fix up all additional identified columns like so, be careful to verify each table and column as this may not be an exhaustive list (also be aware of `entity_id`, `row_id` and `id`)
78 | 1. `php bin/magento gene:encryption-key-manager:reencrypt-column admin_user user_id rp_token`
79 | 2. `php bin/magento gene:encryption-key-manager:reencrypt-column customer_entity entity_id rp_token`
80 | 3. `php bin/magento gene:encryption-key-manager:reencrypt-column oauth_token entity_id secret`
81 | 4. `php bin/magento gene:encryption-key-manager:reencrypt-column oauth_consumer entity_id secret`
82 | 5. `php bin/magento gene:encryption-key-manager:reencrypt-column admin_adobe_ims_webapi id access_token`
83 | 6. `php bin/magento gene:encryption-key-manager:reencrypt-column adobe_user_profile id access_token`
84 | 6. Flush the cache `php bin/magento cache:flush`
85 | 6. At this point you should have all your data migrated to your new encryption key, to help you verify this you can do the following
86 | 1. `php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_only_log_old_decrypts 1`
87 | 2. `php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_enable_decrypt_logging 1`
88 | 3. Monitor your logs for "gene encryption manager" to verify nothing is still using the old key
89 | 7. When you are happy you can **invalidate your old key** `php bin/magento gene:encryption-key-manager:invalidate`
90 | 1. `Magento\Catalog\Model\View\Asset\Image` will continue to use the key at the `0` index in the `crypt/invalidated_key` section
91 | 6. Test, test test! Your areas of focus for testing include
92 | - all integrations that use Magento's APIs
93 | - your media should still be displaying with the same hash directory. If it is regenerating it would take up a large amount of disk space and runtime.
94 | - admin user login/logout
95 | - customer login/logout
96 |
97 | # Features
98 |
99 | ## Automatically invalidates old JWTs when a new key is generated
100 | When magento generates a new encryption key it still allows the old one to be used with JWTs. This module prevents that by updating `\Magento\JwtUserToken\Model\SecretBasedJwksFactory` to only allow keys generated against the most recent encryption key.
101 |
102 | We inject a wrapped `\Gene\EncryptionKeyManager\Model\DeploymentConfig` which only returns the most recent encryption key. This means that any existing tokens are no longer usable when a new encryption key is generated.
103 |
104 | ## Allows you to keep your existing media cache directories
105 | When magento generates a new encryption key, it causes the product media cache hash to change. This causes all product media to regenerate which takes a lot of processing time which can slow down page loads for your customers, as well as consuming extra disk space. This module ensures the old hash is still used for the media gallery.
106 |
107 | Magento stores resized product images in directories like `media/catalog/product/cache/abc123/f/o/foobar.jpg`, the hash `abc123` is generated utilising the encryption keys in the system.
108 |
109 | To avoid having to regenerate all the product media when cycling the encryption key there are some changes to force it to continue using the original value.
110 |
111 | `Magento\Catalog\Model\View\Asset\Image` has the `$encryptor` swapped out with `Gene\EncryptionKeyManager\Service\InvalidatedKeyHasher`. Which allows you to continue to generate md5 hashes with the old key.
112 |
113 | ## Prevents long running process updating order payments
114 | This module will also fix an issue where every `sales_order_payment` entry was updated during the key generation process. On large stores this could take a long time. Now only necessary entries with saved card information are updated.
115 |
116 | ## Logging
117 |
118 | This module provides a mechanism to log the source for each call to decrypt. This will produce a lot of data as the magento system configuration is encrypted so each request will trigger a log write.
119 |
120 | It is recommended to enable this logging after you have handled the re-encryption of all data in your system, but _before_ you invalidate the old keys. This will give you an indication that you have properly handled everything, because if you see a log written it will tell you that something has been missed and where to go to find the source of the issue.
121 |
122 | ```bash
123 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_only_log_old_decrypts 1
124 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_enable_decrypt_logging 1
125 | ```
126 |
127 | The log file is located in /var/log/gene_encryption_key.log
128 |
129 | ## bin/magento gene:encryption-key-manager:generate
130 |
131 | You can use `php bin/magento gene:encryption-key-manager:generate` to generate a new encryption key
132 |
133 | This CLI tool does the same tasks as `\Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key\Save::execute()` with a few tweaks
134 | - Avoids unneeded manipulation of empty `sales_order_payment` `cc_number_enc` values. This can he helpful on large stores with many items in this table.
135 |
136 | ```bash
137 | $ php bin/magento gene:encryption-key-manager:generate --force
138 | Generating a new encryption key
139 | _reEncryptSystemConfigurationValues - start
140 | _reEncryptSystemConfigurationValues - end
141 | _reEncryptCreditCardNumbers - start
142 | _reEncryptCreditCardNumbers - end
143 | Cleaning cache
144 | Done
145 | ```
146 |
147 | - Use the `--key` option to manually define the new key to use during re-encryption. If no custom key is provided, a new key will be generated.
148 | - Use the `--skip-saved-credit-cards` flag to skip re-encrypting the `sales_order_payment` `cc_number_enc` data. This table can be very large, and many stores will have no data saved in this column.
149 | - This will automatically re-encrypt any `system` values in `app/etc/env.php`
150 |
151 | ## bin/magento gene:encryption-key-manager:invalidate
152 |
153 | You can use `php bin/magento gene:encryption-key-manager:invalidate` to invalidate old keys
154 |
155 | This will create a new section to store the old `invalidated_key` within your `env.php` as well as stub out the `crypt/key` path with nonsense text, so that the numerical ordering of the keys is maintained.
156 |
157 | Before invalidation
158 | ```php
159 | 'crypt' => [
160 | 'key' => '84c9d7c0b305adf9ea7e19a05478bf11
161 | 2951b41e2b7f4c26e60a8e7ee00ca17b'
162 | ],
163 | ```
164 |
165 | After invalidation
166 | ```php
167 | 'crypt' => [
168 | 'key' => 'invalidpwecbVeGpoL3Jxa4PXEOdn1ej
169 | 2951b41e2b7f4c26e60a8e7ee00ca17b',
170 | 'invalidated_key' => '84c9d7c0b305adf9ea7e19a05478bf11'
171 | ],
172 | ```
173 |
174 | ## bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data
175 |
176 | When Magento generates a new encryption key it re-encrypts values in `core_config_data` where the `backend_model` is defined as `Magento\Config\Model\Config\Backend\Encrypted`. It is likely some third party modules have not implemented this correctly and handled the decryption themselves. In these cases we need to force through the re-encryption process for them.
177 |
178 | This command runs in dry run mode by default, do that as a first pass to see the changes that will be made. When you are happy run with `--force`.
179 |
180 | ```bash
181 | $ php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data
182 | Run with --force to make these changes, this will run in dry-run mode by default
183 | The latest encryption key is number 14, looking for old entries
184 | ################################################################################
185 | config_id: 1347
186 | scope: default
187 | scope_id: 0
188 | path: yotpo/settings/secret
189 | updated_at: 2023-08-31 12:48:27
190 | ciphertext_old: 0:2:abc123
191 | plaintext: some_secret_here
192 | ciphertext_new: 14:3:xyz456
193 | Dry run mode, no changes have been made
194 | ################################################################################
195 | Done
196 | ```
197 |
198 | ## bin/magento gene:encryption-key-manager:reencrypt-column
199 |
200 | This allows you to target a specific column for re-encryption. If the column contains JSON, you can target it using dot notation: `column.field`.
201 |
202 | This command runs in dry run mode by default, do that as a first pass to see the changes that will be made. When you are happy run with `--force`.
203 |
204 | You should identify all columns that need to be handled, and run them through this process.
205 |
206 | ```bash
207 | $ bin/magento gene:encryption-key-manager:reencrypt-column customer_entity entity_id rp_token
208 | Run with --force to make these changes, this will run in dry-run mode by default
209 | The latest encryption key is number 1, looking for old entries
210 | Looking for 'rp_token' in 'customer_entity', identified by 'entity_id'
211 | ########################################################################################################################
212 | entity_id: 9876
213 | ciphertext_old: 0:3:54+QHWqhSwuncAa87Ueph7xF9qPL1CT6+M9Z5AWuup447J33KGVw+Q+BvVLSKR1H1umiq69phKq5NEHk
214 | plaintext: acb123
215 | ciphertext_new: 1:3:Y52lxB2VDnKeOHa0hf7kG/d15oooib6GQOYTcAmzfuEnhfW64NAdNN4YjRrhlh2IzQBO5IbwS48JDDRh
216 | Dry run mode, no changes have been made
217 | ########################################################################################################################
218 | Done
219 | ```
220 |
221 | ## bin/magento gene:encryption-key-manager:reencrypt-tfa-data
222 |
223 | This command re-encrypts the 2FA data stored in `tfa_user_config`. Some of this data is doubly encrypted, which is why it needs special handling.
224 |
225 | This command runs in dry run mode by default, do that as a first pass to see the changes that will be made. When you are happy run with `--force`.
226 |
227 | This CLI has only been tested with Google Authenticator (TOTP) and U2F (Yubikey, etc). If you use Authy or DUO you *MUST* verify before use.
228 |
229 | ```bash
230 | $ bin/magento gene:encryption-key-manager:reencrypt-tfa-data
231 | Run with --force to make these changes, this will run in dry-run mode by default
232 | This CLI has only been tested with Google Authenticator (TOTP) and U2F (Yubikey, etc). If you use Authy or DUO you *MUST* verify before use.
233 | The latest encryption key is number 1, looking for old entries
234 | Looking for encoded_config in tfa_user_config, identified by 'config_id'
235 | ########################################################################################################################
236 | config_id: 1
237 | ciphertext_old: 0:3:rV/z9+ilmOtaPGnOoZBayZ3waBNphK1RAcyWLetipM5UONn793rTyRknO1GhWxKxXC3ooJAgWDTMJPaXGRMGdj8yOqrlrjEp9uqi8D9SFgE/UTiWkBF4RRwVvZeo4lGGnll/CxJmtzuMXWa65TS0Z/a2QLdPyIH/3OomJH7sb3FgfQ==
238 | plaintext_old: {"google":{"secret":"0:3:LKm9642Rpl0gqlBha+m3FYWnQBBtLgjdLDvjfoPo923xmxd9ykbnvX0LucKI","active":true}}
239 | plaintext_new: {"google":{"secret":"1:3:m9mScDkTkeCdn2lXpwf5oMkL7lmgLOTYJXQyKbK\/m8QwDZVDNWI3CzH+uBaq","active":true}}
240 | ciphertext_new: 1:3:Tw/5ik2meBqzL8oodrudxmksrOekA/DbZE5+KgBAygFxp6Zx/A7vbMyHt4+N1MtQhlnqW/mAXL3l2kDpFHIQVvi2L+23o9mRpii2ldBwmuZgDlpQsm+Q4Hf8a+t2aUKndGOMeoH6xcZXFCConC+TUI+uregFXx6B5LU4ohCY52m/v7w=
241 | Dry run mode, no changes have been made
242 | ########################################################################################################################
243 | Done
244 | ```
245 |
246 | ## bin/magento gene:encryption-key-manager:get-cloud-keys
247 |
248 | This command to get re-encrypted cloud environments variables.
249 | This one DOESN'T update existing values, it just returns new ones in console.
250 | The Dev has to update them manually in cloud console.
251 |
252 | ```bash
253 | # No keys example
254 | $ bin/magento gene:encryption-key-manager:get-cloud-keys
255 | There is no old encrypted environment variables found
256 |
257 | # There is some encoded
258 | $ bin/magento gene:encryption-key-manager:get-cloud-keys --show-decrypted
259 | There is no old encrypted environment variables found
260 | The CLI command doesn\'t rewrite values. You have to update them manually in cloud console!
261 | Rows count: 4
262 | ##################################################################
263 | Name: CONFIG__DEFAULT__SOME_KEY
264 | Dectypted value: dectypted_value
265 | Old Encrypted Value: 0:3:AAA1
266 | New Encrypted Value: 1:3:BBB1
267 | ##################################################################
268 | Name: CONFIG__DEFAULT__SOME_KEY_2
269 | Dectypted value: dectypted_value_2
270 | Old Encrypted Value: 0:3:AAA2
271 | New Encrypted Value: 1:3:BBB2
272 | ```
273 |
274 | # Caveats
275 |
276 | ## report.WARNING: Unable to unserialize value
277 |
278 | This is not common, it has been reported by people using this module and people using the admin controller to rotate their encryption keys. Flushing redis cache resolves the issue.
279 |
280 | Please ensure you [flush your redis cache](https://redis.io/docs/latest/commands/flushall/)
281 |
282 | Now you are right to continue with the re-encryption work as stated above.
283 |
284 | ## You manually replaced your encryption key
285 | You will need to:
286 | 1. Recover your old encryption key
287 | 1. Prepend your old key to the new key, separated by `\n` and repeat the steps above
288 |
289 | ## Other issues
290 |
291 | Search https://github.com/genecommerce/module-encryption-key-manager/issues for other issues.
292 |
--------------------------------------------------------------------------------
/Service/ChangeEncryptionKey.php:
--------------------------------------------------------------------------------
1 | output = $output;
23 | }
24 |
25 | /**
26 | * @param bool $skipSavedCreditCards
27 | * @return void
28 | */
29 | public function setSkipSavedCreditCards($skipSavedCreditCards)
30 | {
31 | $this->skipSavedCreditCards = (bool) $skipSavedCreditCards;
32 | }
33 |
34 | /**
35 | * @param string $text
36 | * @return void
37 | */
38 | private function writeOutput($text)
39 | {
40 | if ($this->output instanceof OutputInterface) {
41 | $this->output->writeln($text);
42 | }
43 | }
44 |
45 | /**
46 | * Gather all encrypted system config values and re-encrypt them
47 | *
48 | * @return void
49 | */
50 | protected function _reEncryptSystemConfigurationValues()
51 | {
52 | $this->writeOutput('_reEncryptSystemConfigurationValues - start');
53 | parent::_reEncryptSystemConfigurationValues();
54 | $this->writeOutput('_reEncryptSystemConfigurationValues - end');
55 | }
56 |
57 | /**
58 | * Gather saved credit card numbers from sales order payments and re-encrypt them
59 | *
60 | * The parent function does not handle null values, so this version filters them out as well as adding CLI output
61 | *
62 | * @return void
63 | */
64 | protected function _reEncryptCreditCardNumbers()
65 | {
66 | if ($this->skipSavedCreditCards) {
67 | $this->writeOutput('_reEncryptCreditCardNumbers - skipping');
68 | return;
69 | }
70 | $this->writeOutput('_reEncryptCreditCardNumbers - start');
71 | $table = $this->getTable('sales_order_payment');
72 | $select = $this->getConnection()->select()->from($table, ['entity_id', 'cc_number_enc']);
73 |
74 | $attributeValues = $this->getConnection()->fetchPairs($select);
75 | // save new values
76 | foreach ($attributeValues as $valueId => $value) {
77 | // GENE CHANGE START
78 | if (!$value) {
79 | continue;
80 | }
81 | // GENE CHANGE END
82 | $this->getConnection()->update(
83 | $table,
84 | ['cc_number_enc' => $this->encryptor->encrypt($this->encryptor->decrypt($value))],
85 | ['entity_id = ?' => (int)$valueId]
86 | );
87 | }
88 | $this->writeOutput('_reEncryptCreditCardNumbers - end');
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Service/InvalidatedKeyHasher.php:
--------------------------------------------------------------------------------
1 | scopeConfig->getValue('gene/encryption_key_manager/invalidated_key_index');
34 | if ($keyIndex === null) {
35 | return;
36 | }
37 | $this->keyVersion = (int) $keyIndex;
38 |
39 | $invalidatedKeys = array_filter(preg_split('/\s+/s', trim((string)$deploymentConfig->get('crypt/invalidated_key'))));
40 | if (!empty($invalidatedKeys)) {
41 | $this->keys = $invalidatedKeys;
42 | }
43 | }
44 |
45 | /**
46 | * @throws \LogicException
47 | */
48 | private function fail()
49 | {
50 | throw new \LogicException('You can only use this class for the "hash" function with invalidated keys');
51 | }
52 |
53 | /**
54 | * @inheritdoc
55 | * @throws \LogicException
56 | */
57 | public function getHash($password, $salt = false, $version = self::HASH_VERSION_LATEST)
58 | {
59 | return $this->fail();
60 | }
61 |
62 | /**
63 | * @inheritdoc
64 | * @throws \LogicException
65 | */
66 | public function isValidHash($password, $hash)
67 | {
68 | return $this->fail();
69 | }
70 |
71 | /**
72 | * @inheritdoc
73 | * @throws \LogicException
74 | */
75 | public function validateHashVersion($hash, $validateCount = false)
76 | {
77 | return $this->fail();
78 | }
79 |
80 | /**
81 | * @inheritdoc
82 | * @throws \LogicException
83 | */
84 | public function encrypt($data)
85 | {
86 | return $this->fail();
87 | }
88 |
89 | /**
90 | * @inheritdoc
91 | * @throws \LogicException
92 | */
93 | public function decrypt($data)
94 | {
95 | return $this->fail();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Service/ReencryptEnvSystemConfigurationValues.php:
--------------------------------------------------------------------------------
1 | deploymentConfig->resetData();
47 | $this->encryptor = $this->encryptorFactory->create();
48 | $systemConfig = $this->deploymentConfig->get('system');
49 | if (!$systemConfig) {
50 | return;
51 | }
52 | $systemConfig = $this->iterateSystemConfig($systemConfig);
53 |
54 | $encryptSegment = new ConfigData(ConfigFilePool::APP_ENV);
55 | $encryptSegment->set('system', $systemConfig);
56 | $this->writer->saveConfig([$encryptSegment->getFileKey() => $encryptSegment->getData()]);
57 |
58 | /**
59 | * @see \Magento\Deploy\Console\Command\App\ConfigImport\Processor::execute()
60 | */
61 | $this->hash->regenerate('system');
62 | }
63 |
64 | /**
65 | * Recursively iterate through the system configuration and re-encrypt any encrypted values
66 | *
67 | * @param array $systemConfig
68 | * @return array
69 | * @throws \Exception
70 | */
71 | private function iterateSystemConfig(array $systemConfig): array
72 | {
73 | foreach ($systemConfig as $key => &$value) {
74 | if (is_array($value)) {
75 | $value = $this->iterateSystemConfig($value);
76 | } elseif (is_string($value) && preg_match('/^\d+:\d+:.*$/', $value)) {
77 | $decryptedValue = $this->encryptor->decrypt($value);
78 | if ($decryptedValue) {
79 | $value = $this->encryptor->encrypt($decryptedValue);
80 | }
81 | }
82 | }
83 |
84 | return $systemConfig;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gene/module-encryption-key-manager",
3 | "description": "Gene encryption key manager",
4 | "type": "magento2-module",
5 | "license": "LGPL-3.0-only",
6 | "require": {
7 | "php": "^8.1||^8.2||^8.3|^8.4",
8 | "magento/module-jwt-user-token": "*",
9 | "magento/module-encryption-key": "*",
10 | "magento/module-catalog": "*"
11 | },
12 | "autoload": {
13 | "files": [
14 | "registration.php"
15 | ],
16 | "psr-4": {
17 | "Gene\\EncryptionKeyManager\\": ""
18 | }
19 | },
20 | "minimum-stability": "dev",
21 | "prefer-stable": true
22 | }
23 |
--------------------------------------------------------------------------------
/dev/README.md:
--------------------------------------------------------------------------------
1 | These tests are not what we would like long term, but for a quick win they have been added as a shell script.
2 |
3 | To run the tests
4 | ```bash
5 | # Get set up with the module
6 | git clone https://github.com/genecommerce/module-encryption-key-manager
7 | git clone https://github.com/AmpersandHQ/magento-docker-test-instance --branch 0.1.21
8 | cd magento-docker-test-instance
9 |
10 | # Install magento
11 | CURRENT_EXTENSION="../module-encryption-key-manager" FULL_INSTALL=1 ./bin/mtest-make 2-4-6-p3
12 |
13 | # Setup tests
14 | ./bin/mtest 'cp vendor/gene/module-encryption-key-manager/dev/test.sh .'
15 | ./bin/mtest 'chmod +x ./test.sh'
16 |
17 | # Run tests
18 | ./bin/mtest './test.sh'
19 | ```
20 |
--------------------------------------------------------------------------------
/dev/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | err_report() {
4 | echo "Error on line $1"
5 | echo "last test.txt was"
6 | cat test.txt
7 | echo "last app/etc/env.php was"
8 | cat app/etc/env.php
9 | echo "restoring original env.php"
10 | cp app/etc/env.php.bak app/etc/env.php
11 | }
12 | trap 'err_report $LINENO' ERR
13 | echo "backing up initial app/etc/env.php"
14 | rm -rf var/cache
15 | cp app/etc/env.php app/etc/env.php.bak
16 | php bin/magento app:config:import
17 |
18 | URL='http://0.0.0.0:1234/'
19 | CURRENT_TIMESTAMP=$(date +%s)
20 | ADMIN="adminuser$CURRENT_TIMESTAMP"
21 | PASSWORD='password123'
22 |
23 | echo "Putting magento into production mode, di:compile has already been generated"
24 | php bin/magento deploy:mode:set production -s
25 | php -d memory_limit=-1 bin/magento setup:static-content:deploy en_US --no-interaction -f --no-ansi --area=frontend
26 |
27 | echo "Verifying frontend is functional"
28 | php bin/magento cache:disable full_page
29 | curl "$URL" -vvv > test.txt 2>&1
30 | grep -q '200 OK' test.txt
31 | grep --max-count=1 'static/version' test.txt
32 | grep -q 'All rights reserved.' test.txt
33 | echo "PASS"
34 | echo "";echo "";
35 |
36 | echo "Stubbing in some test data"
37 | vendor/bin/n98-magerun2 --version
38 | vendor/bin/n98-magerun2 admin:user:create --no-interaction --admin-user "$ADMIN" --admin-email "example$CURRENT_TIMESTAMP@example.com" --admin-password $PASSWORD --admin-firstname adminuser --admin-lastname adminuser
39 | vendor/bin/n98-magerun2 config:store:set zzzzz/zzzzz/zzzz xyz123 --encrypt
40 |
41 | echo "Spoofing an encrypted config value into env.php"
42 | ENCRYPTED_ENV_VALUE=$(vendor/bin/n98-magerun2 dev:encrypt 'Some Base Name')
43 | bin/magento config:set --lock-env general/store_information/name "$ENCRYPTED_ENV_VALUE"
44 |
45 | ADMIN_ID=$(vendor/bin/n98-magerun2 db:query "SELECT user_id FROM admin_user LIMIT 1")
46 | ADMIN_ID="${ADMIN_ID: -1}"
47 | FAKE_GOOGLE_TOKEN=$(vendor/bin/n98-magerun2 dev:encrypt 'googletokenabc123')
48 | TWOFA_JSON="{\"google\":{\"secret\":\"$FAKE_GOOGLE_TOKEN\",\"active\":true}}"
49 | TWOFA_JSON_ENCRYPTED=$(vendor/bin/n98-magerun2 dev:encrypt "$TWOFA_JSON")
50 | echo "Generating 2FA data for admin user $ADMIN in tfa_user_config"
51 | vendor/bin/n98-magerun2 db:query "delete from tfa_user_config where user_id=$ADMIN_ID";
52 | vendor/bin/n98-magerun2 db:query "insert into tfa_user_config(user_id, encoded_config) values ($ADMIN_ID, '$TWOFA_JSON_ENCRYPTED');"
53 | vendor/bin/n98-magerun2 db:query "select user_id, encoded_config from tfa_user_config where user_id=$ADMIN_ID";
54 |
55 | FAKE_RP_TOKEN=$(vendor/bin/n98-magerun2 dev:encrypt 'abc123')
56 | vendor/bin/n98-magerun2 db:query "update admin_user set rp_token='$FAKE_RP_TOKEN' where username='$ADMIN'"
57 | echo "Generated FAKE_RP_TOKEN=$FAKE_RP_TOKEN and assigned to $ADMIN"
58 |
59 | echo "Generating a fake json column"
60 | FAKE_JSON_PASSWORD=$(vendor/bin/n98-magerun2 dev:encrypt 'jsonpasswordabc123')
61 | FAKE_JSON_USERNAME=$(vendor/bin/n98-magerun2 dev:encrypt 'foobar')
62 | FAKE_JSON_PAYLOAD="{\"username\": \"$FAKE_JSON_USERNAME\", \"password\": \"$FAKE_JSON_PASSWORD\", \"request_url\": \"\"}"
63 | vendor/bin/n98-magerun2 db:query 'DROP TABLE IF EXISTS fake_json_table; CREATE TABLE fake_json_table (id INT AUTO_INCREMENT PRIMARY KEY, text_column TEXT);'
64 | vendor/bin/n98-magerun2 db:query "insert into fake_json_table(text_column) values ('$FAKE_JSON_PAYLOAD');"
65 | vendor/bin/n98-magerun2 db:query "select * from fake_json_table";
66 |
67 | echo "";echo "";
68 |
69 | echo "Verifying commands need to use --force"
70 |
71 | php bin/magento gene:encryption-key-manager:generate > test.txt || true;
72 | if grep -q 'Run with --force' test.txt; then
73 | echo "PASS: generate needs to run with force"
74 | else
75 | echo "FAIL: generate needs to run with force" && false
76 | fi
77 |
78 | php bin/magento gene:encryption-key-manager:invalidate > test.txt || true
79 | if grep -q 'Run with --force' test.txt; then
80 | echo "PASS: invalidate needs to run with force"
81 | else
82 | echo "FAIL: invalidate needs to run with force" && false
83 | fi
84 |
85 | php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data > test.txt || true
86 | if grep -q 'Run with --force' test.txt; then
87 | echo "PASS: reencrypt-unhandled-core-config-data needs to run with force"
88 | else
89 | echo "FAIL: reencrypt-unhandled-core-config-data needs to run with force" && false
90 | fi
91 |
92 | php bin/magento gene:encryption-key-manager:reencrypt-tfa-data > test.txt || true
93 | if grep -q 'Run with --force' test.txt; then
94 | echo "PASS: reencrypt-tfa-data needs to run with force"
95 | else
96 | echo "FAIL: reencrypt-tfa-data needs to run with force" && false
97 | fi
98 |
99 | php bin/magento gene:encryption-key-manager:reencrypt-column admin_user user_id rp_token > test.txt || true
100 | if grep -q 'Run with --force' test.txt; then
101 | echo "PASS: reencrypt-column needs to run with force"
102 | else
103 | echo "FAIL: reencrypt-column needs to run with force" && false
104 | fi
105 | echo "";echo "";
106 |
107 | echo "Verifying you cannot invalidate with only 1 key"
108 | php bin/magento gene:encryption-key-manager:invalidate --force > test.txt || true
109 | if grep -Eq 'Cannot invalidate when there is only one key|No further keys need invalidated' test.txt; then
110 | echo "PASS: You cannot invalidate with only 1 key"
111 | else
112 | echo "FAIL" && false
113 | fi
114 | echo "";echo "";
115 |
116 | echo "Generating a new encryption key"
117 | grep -q "$ENCRYPTED_ENV_VALUE" app/etc/env.php
118 | php bin/magento gene:encryption-key-manager:generate --force > test.txt
119 | if grep -q "$ENCRYPTED_ENV_VALUE" app/etc/env.php; then
120 | echo "FAIL: The old encrypted value in env.php was not updated" && false
121 | fi
122 | grep "'name'" app/etc/env.php | grep -q "1:3:"
123 | grep -q '_reEncryptSystemConfigurationValues - start' test.txt
124 | grep -q '_reEncryptSystemConfigurationValues - end' test.txt
125 | grep -q '_reEncryptCreditCardNumbers - start' test.txt
126 | grep -q '_reEncryptCreditCardNumbers - end' test.txt
127 | echo "PASS"
128 | echo "";echo "";
129 |
130 | echo "Generating a new encryption key - skipping _reEncryptCreditCardNumbers"
131 | php bin/magento gene:encryption-key-manager:generate --force --skip-saved-credit-cards > test.txt
132 | grep "'name'" app/etc/env.php | grep -q "2:3:"
133 | grep -q '_reEncryptSystemConfigurationValues - start' test.txt
134 | grep -q '_reEncryptSystemConfigurationValues - end' test.txt
135 | grep -q '_reEncryptCreditCardNumbers - skipping' test.txt
136 | if grep -q '_reEncryptCreditCardNumbers - start' test.txt; then
137 | echo "FAIL: We should never start on _reEncryptCreditCardNumbers with --skip-saved-credit-cards" && false
138 | fi
139 | if grep -q '_reEncryptCreditCardNumbers - end' test.txt; then
140 | echo "FAIL: We should never end on _reEncryptCreditCardNumbers with --skip-saved-credit-cards" && false
141 | fi
142 | echo "PASS"
143 | echo "";echo "";
144 |
145 | echo "Verifying frontend is still functional after key generation"
146 | php bin/magento cache:flush
147 | curl "$URL?test1" -vvv > test.txt 2>&1
148 | grep -q '200 OK' test.txt
149 | grep --max-count=1 'static/version' test.txt
150 | grep -q 'All rights reserved.' test.txt
151 | echo "PASS"
152 | echo "";echo "";
153 |
154 | echo "Running reencrypt-unhandled-core-config-data"
155 | php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data --force > test.txt
156 | cat test.txt
157 | grep -q 'zzzzz/zzzzz/zzzz' test.txt
158 | grep -q 'xyz123' test.txt
159 | echo "PASS"
160 | echo "";echo "";
161 | echo "Running reencrypt-unhandled-core-config-data - again to verify it was all processed"
162 | php bin/magento gene:encryption-key-manager:reencrypt-unhandled-core-config-data --force | grep --context 999 'No old entries found'
163 | echo "PASS"
164 | echo "";echo "";
165 |
166 | echo "Running reencrypt-tfa-data"
167 | php bin/magento gene:encryption-key-manager:reencrypt-tfa-data --force > test.txt
168 | cat test.txt
169 | grep 'plaintext_new' test.txt | grep 'secret' test.txt
170 | if grep 'plaintext_new' test.txt | grep "$TWOFA_JSON_ENCRYPTED"; then
171 | echo "FAIL: The plaintext_new should no longer have the original TWOFA_JSON_ENCRYPTED data" && false
172 | else
173 | echo "PASS: The plaintext_new should no longer have the original TWOFA_JSON_ENCRYPTED data"
174 | fi
175 | echo "PASS"
176 | echo "";echo "";
177 | echo "Running reencrypt-tfa-data - again to verify it was all processed"
178 | php bin/magento gene:encryption-key-manager:reencrypt-tfa-data --force | grep --context 999 'No old entries found'
179 | echo "PASS"
180 | echo "";echo "";
181 |
182 | echo "Running reencrypt-column"
183 | php bin/magento gene:encryption-key-manager:reencrypt-column admin_user user_id rp_token --force > test.txt
184 | cat test.txt
185 | grep -q "$FAKE_RP_TOKEN" test.txt
186 | grep -q abc123 test.txt
187 | echo "PASS"
188 | echo "";echo "";
189 | echo "Running reencrypt-column - again to verify it was all processed"
190 | php bin/magento gene:encryption-key-manager:reencrypt-column admin_user user_id rp_token --force | grep --context 999 'No old entries found'
191 | echo "PASS"
192 | echo "";echo "";
193 |
194 | echo "Running reencrypt-column on JSON column username"
195 | php bin/magento gene:encryption-key-manager:reencrypt-column fake_json_table id text_column.username --force > test.txt
196 | cat test.txt
197 | grep -q "$FAKE_JSON_USERNAME" test.txt
198 | grep -q foobar test.txt
199 | echo "PASS"
200 | echo "";echo "";
201 | echo "Running reencrypt-column on JSON column username - again to verify it was all processed"
202 | php bin/magento gene:encryption-key-manager:reencrypt-column fake_json_table id text_column.username --force | grep --context 999 'No old entries found'
203 | echo "PASS"
204 | echo "";echo "";
205 |
206 | echo "Running reencrypt-column on JSON column password to validate multiple fields can be re-encrypted"
207 | php bin/magento gene:encryption-key-manager:reencrypt-column fake_json_table id text_column.password --force > test.txt
208 | cat test.txt
209 | grep -q "$FAKE_JSON_PASSWORD" test.txt
210 | grep -q jsonpasswordabc123 test.txt
211 | echo "PASS"
212 | echo "";echo "";
213 | echo "Running reencrypt-column on JSON column password to validate multiple fields can be re-encrypted - again to verify it was all processed"
214 | php bin/magento gene:encryption-key-manager:reencrypt-column fake_json_table id text_column.password --force | grep --context 999 'No old entries found'
215 | echo "PASS"
216 | echo "";echo "";
217 |
218 | echo "Running invalidate"
219 | php bin/magento gene:encryption-key-manager:invalidate --force
220 | grep -q invalidated_key app/etc/env.php
221 | php bin/magento gene:encryption-key-manager:invalidate --force | grep --context 999 'No further keys need invalidated'
222 | echo "PASS"
223 | echo "";echo "";
224 |
225 | echo "Testing the decrypt logger"
226 |
227 | echo "Testing disable behaviour"
228 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_enable_decrypt_logging 0
229 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_only_log_old_decrypts 0
230 | php bin/magento cache:flush; php bin/magento | head -1; # clear and warm caches
231 | rm -f var/log/*.log && php bin/magento | head -1 # trigger a decrypt of the stored system config
232 | touch var/log/gene_encryption_key.log
233 | ls -l var/log
234 | if grep -q 'gene encryption manager' var/log/gene_encryption_key.log; then
235 | cat var/log/gene_encryption_key.log
236 | echo "FAIL: No logs should be produced without enabling the logger" && false
237 | else
238 | echo "PASS: No logs were produced"
239 | fi
240 | echo "";echo "";
241 |
242 | echo "Testing that enabling it produces a log"
243 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_enable_decrypt_logging 1
244 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_only_log_old_decrypts 0
245 | php bin/magento cache:flush; php bin/magento | head -1; # clear and warm caches
246 | rm -f var/log/*.log && php bin/magento | head -1 # trigger a decrypt of the stored system config
247 | touch var/log/gene_encryption_key.log
248 | ls -l var/log
249 | if grep 'gene encryption manager' var/log/gene_encryption_key.log | grep -q 'Magento\\'; then
250 | echo "PASS: A log was produced"
251 | else
252 | cat var/log/gene_encryption_key.log
253 | echo "FAIL: A log should be produced" && false
254 | fi
255 | echo "";echo "";
256 |
257 | echo "Testing that gene_encryption_manager_only_log_old_decrypts=1 stops a log being written"
258 | php bin/magento config:set --lock-env dev/debug/gene_encryption_manager_only_log_old_decrypts 1
259 | php bin/magento cache:flush; php bin/magento | head -1; # clear and warm caches
260 | rm -f var/log/*.log && php bin/magento | head -1 # trigger a decrypt of the stored system config
261 | touch var/log/gene_encryption_key.log
262 | ls -l var/log
263 | if grep -q 'gene encryption manager' var/log/gene_encryption_key.log; then
264 | cat var/log/gene_encryption_key.log
265 | echo "FAIL: No logs should be produced when the keys are up to date" && false
266 | else
267 | echo "PASS: No logs were produced when the keys were up to date"
268 | fi
269 | echo "";echo "";
270 |
271 | echo "Testing that gene_encryption_manager_only_log_old_decrypts=1 writes when an old key is used"
272 | rm -f var/log/*.log && php bin/magento | head -1 # trigger a decrypt of the stored system config
273 | vendor/bin/n98-magerun2 dev:decrypt '0:3:qwertyuiopasdfghjklzxcvbnm' # we are on a higher key than 0 now
274 | touch var/log/gene_encryption_key.log
275 | ls -l var/log
276 | if grep 'gene encryption manager' var/log/gene_encryption_key.log | grep -q 'DecryptCommand'; then
277 | echo "PASS: We have a log hit when trying to decrypt with the old key"
278 | else
279 | cat var/log/gene_encryption_key.log
280 | echo "FAIL: We should have a log hit when trying to decrypt using an old key" && false
281 | fi
282 | echo "";echo "";
283 |
284 | echo "Verifying that the log is not present in system.log"
285 | touch var/log/system.log
286 | if grep 'gene encryption manager' var/log/system.log | grep -q 'DecryptCommand'; then
287 | cat var/log/system.log
288 | echo "FAIL: The log is also present in system.log" && false
289 | else
290 | echo "PASS: The log is not present in system.log"
291 | fi
292 |
293 | echo "Verifying frontend is still functional after all the tests"
294 | php bin/magento cache:flush
295 | curl "$URL?test2" -vvv > test.txt 2>&1
296 | grep -q '200 OK' test.txt
297 | grep --max-count=1 'static/version' test.txt
298 | grep -q 'All rights reserved.' test.txt
299 | echo "PASS"
300 | echo "";echo "";
301 |
302 | echo "A peek at an example log"
303 | grep 'gene encryption manager' var/log/gene_encryption_key.log | tail -1
304 |
305 | echo "A peek at the env.php"
306 | grep "'name'" app/etc/env.php
307 | grep -A10 "'crypt' =>" app/etc/env.php
308 | echo "";echo "";
309 |
310 | echo "restoring original env.php"
311 | cp app/etc/env.php.bak app/etc/env.php
312 | echo "DONE"
313 |
--------------------------------------------------------------------------------
/etc/adminhtml/system.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Magento\Config\Model\Config\Source\Yesno
9 |
10 |
11 |
12 | Magento\Config\Model\Config\Source\Yesno
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/etc/di.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Gene\EncryptionKeyManager\Model\DeploymentConfig
11 |
12 |
13 |
14 |
15 |
16 | Gene\EncryptionKeyManager\Service\InvalidatedKeyHasher
17 |
18 |
19 |
20 |
21 |
22 | Gene\EncryptionKeyManager\Service\ChangeEncryptionKey\Proxy
23 |
24 |
25 |
26 |
27 |
28 | Magento\Config\Model\Config\StructureLazy
29 |
30 |
31 |
32 |
33 |
34 |
35 | Gene\EncryptionKeyManager\Console\GenerateEncryptionKey
36 | Gene\EncryptionKeyManager\Console\InvalidateOldEncryptionKeys
37 | Gene\EncryptionKeyManager\Console\ReencryptUnhandledCoreConfigData
38 | Gene\EncryptionKeyManager\Console\ReencryptColumn
39 | Gene\EncryptionKeyManager\Console\ReencryptTfaData
40 | Gene\EncryptionKeyManager\Console\GetReEncryptedCloudEnvironmentsKeys
42 |
43 |
44 |
45 |
46 |
47 |
49 |
50 | /var/log/gene_encryption_key.log
51 |
52 |
53 |
54 |
55 |
56 | GeneEncryptionHandler
57 |
58 |
59 |
60 |
61 |
62 | GeneEncryptionLogger
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |