diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index d6e055c43322..e9d17b5c70dd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -39,7 +39,7 @@ - + @@ -60,6 +60,7 @@ + @@ -78,7 +79,7 @@ - + @@ -88,6 +89,7 @@ + diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php index 89f6be052566..e94992ae26b6 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php @@ -78,7 +78,7 @@ protected function _construct() $this->buttonList->add( 'insert_files', - ['class' => 'save no-display primary', 'label' => __('Add Selected'), 'type' => 'button'], + ['class' => 'save no-display action-primary', 'label' => __('Add Selected'), 'type' => 'button'], 0, 0, 'header' diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml index 8c1d17c8d9be..2daa1cbeca80 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SelectImageFromMediaStorageActionGroup.xml @@ -17,7 +17,6 @@ - diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index 5a6dbc8e3189..5c74220051ba 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -281,6 +281,10 @@ protected function _createEmptyElement(array $pathParts) public function getFieldPathsByAttribute($attributeName, $attributeValue) { $result = []; + if (empty($this->_data['sections'])) { + return $result; + } + foreach ($this->_data['sections'] as $section) { if (!isset($section['children'])) { continue; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index 39b2f772c8e8..8ffa5839a125 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -258,6 +258,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'float', + 'store' => true, ], ], ], diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php new file mode 100644 index 000000000000..a1fcbeb06148 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php @@ -0,0 +1,251 @@ +connectionManager = $connectionManager; + $this->fieldMapper = $fieldMapper; + $this->clientConfig = $clientConfig; + $this->fieldName = $fieldName; + $this->storeId = $storeId; + $this->entityIds = $entityIds; + $this->searchIndexNameResolver = $searchIndexNameResolver; + } + + /** + * {@inheritdoc} + */ + public function load($limit, $offset = null, $lower = null, $upper = null) + { + $from = $to = []; + if ($lower) { + $from = ['gte' => $lower - self::DELTA]; + } + if ($upper) { + $to = ['lt' => $upper - self::DELTA]; + } + + $requestQuery = $this->prepareBaseRequestQuery($from, $to); + $requestQuery = array_merge_recursive( + $requestQuery, + ['body' => ['stored_fields' => [$this->fieldName], 'size' => $limit]] + ); + + if ($offset) { + $requestQuery['body']['from'] = $offset; + } + + $queryResult = $this->connectionManager->getConnection() + ->query($requestQuery); + + return $this->arrayValuesToFloat($queryResult['hits']['hits'], $this->fieldName); + } + + /** + * {@inheritdoc} + */ + public function loadPrevious($data, $index, $lower = null) + { + if ($lower) { + $from = ['gte' => $lower - self::DELTA]; + } + if ($data) { + $to = ['lt' => $data - self::DELTA]; + } + + $requestQuery = $this->prepareBaseRequestQuery($from, $to); + $requestQuery = array_merge_recursive( + $requestQuery, + ['size' => 0] + ); + + $queryResult = $this->connectionManager->getConnection() + ->query($requestQuery); + + $offset = $queryResult['hits']['total']; + if (!$offset) { + return false; + } + + return $this->load($index - $offset + 1, $offset - 1, $lower); + } + + /** + * {@inheritdoc} + */ + public function loadNext($data, $rightIndex, $upper = null) + { + $from = ['gt' => $data + self::DELTA]; + $to = ['lt' => $data - self::DELTA]; + + $requestCountQuery = $this->prepareBaseRequestQuery($from, $to); + $requestCountQuery = array_merge_recursive( + $requestCountQuery, + ['size' => 0] + ); + + $queryCountResult = $this->connectionManager->getConnection() + ->query($requestCountQuery); + + $offset = $queryCountResult['hits']['total']; + if (!$offset) { + return false; + } + + $from = ['gte' => $data - self::DELTA]; + if ($upper !== null) { + $to = ['lt' => $data - self::DELTA]; + } + + $requestQuery = $requestCountQuery; + + $requestCountQuery['body']['query']['bool']['filter']['bool']['must']['range'] = + [$this->fieldName => array_merge($from, $to)]; + $requestCountQuery['body']['from'] = $offset - 1; + $requestCountQuery['body']['size'] = $rightIndex - $offset + 1; + $queryResult = $this->connectionManager->getConnection() + ->query($requestQuery); + + return array_reverse($this->arrayValuesToFloat($queryResult['hits']['hits'], $this->fieldName)); + } + + /** + * Conver array values to float type. + * + * @param array $hits + * @param string $fieldName + * + * @return float[] + */ + private function arrayValuesToFloat(array $hits, string $fieldName): array + { + $returnPrices = []; + foreach ($hits as $hit) { + $returnPrices[] = (float)$hit['fields'][$fieldName][0]; + } + + return $returnPrices; + } + + /** + * Prepare base query for search. + * + * @param array|null $from + * @param array|null $to + * @return array + */ + private function prepareBaseRequestQuery($from = null, $to = null): array + { + $requestQuery = [ + 'index' => $this->searchIndexNameResolver->getIndexName($this->storeId, Fulltext::INDEXER_ID), + 'type' => $this->clientConfig->getEntityType(), + 'body' => [ + 'stored_fields' => [ + '_id', + ], + 'query' => [ + 'bool' => [ + 'must' => [ + 'match_all' => new \stdClass(), + ], + 'filter' => [ + 'bool' => [ + 'must' => [ + [ + 'terms' => [ + '_id' => $this->entityIds, + ], + ], + [ + 'range' => [ + $this->fieldName => array_merge($from, $to), + ], + ], + ], + ], + ], + ], + ], + 'sort' => [ + $this->fieldName, + ], + ], + ]; + + return $requestQuery; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 415c8b61ac6c..026d385da34d 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -343,7 +343,7 @@ public function testAddFieldsMapping() 'product' => [ '_all' => [ 'enabled' => true, - 'type' => 'text' + 'type' => 'text', ], 'properties' => [ 'name' => [ @@ -356,7 +356,8 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float' + 'type' => 'float', + 'store' => true, ], ], ], @@ -366,7 +367,7 @@ public function testAddFieldsMapping() 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'text', - 'index' => 'no' + 'index' => 'no', ], ], ], @@ -375,7 +376,7 @@ public function testAddFieldsMapping() 'match' => 'position_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'int' + 'type' => 'int', ], ], ], @@ -409,7 +410,7 @@ public function testAddFieldsMappingFailure() 'product' => [ '_all' => [ 'enabled' => true, - 'type' => 'text' + 'type' => 'text', ], 'properties' => [ 'name' => [ @@ -422,7 +423,8 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float' + 'type' => 'float', + 'store' => true, ], ], ], @@ -441,7 +443,7 @@ public function testAddFieldsMappingFailure() 'match' => 'position_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'int' + 'type' => 'int', ], ], ], diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php new file mode 100644 index 000000000000..4030d2dfaf0c --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php @@ -0,0 +1,323 @@ +connectionManager = $this->getMockBuilder(ConnectionManager::class) + ->setMethods(['getConnection']) + ->disableOriginalConstructor() + ->getMock(); + $this->fieldMapper = $this->getMockBuilder(FieldMapperInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->clientConfig = $this->getMockBuilder(Config::class) + ->setMethods([ + 'getIndexName', + 'getEntityType', + ]) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerSession = $this->getMockBuilder(CustomerSession::class) + ->setMethods(['getCustomerGroupId']) + ->disableOriginalConstructor() + ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getCustomerGroupId') + ->willReturn(1); + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchIndexNameResolver = $this + ->getMockBuilder(SearchIndexNameResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeMock->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(1); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->clientConfig->expects($this->any()) + ->method('getIndexName') + ->willReturn('indexName'); + $this->clientConfig->expects($this->any()) + ->method('getEntityType') + ->willReturn('product'); + $this->clientMock = $this->getMockBuilder(ElasticsearchClient::class) + ->setMethods(['query']) + ->disableOriginalConstructor() + ->getMock(); + $this->connectionManager->expects($this->any()) + ->method('getConnection') + ->willReturn($this->clientMock); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + Interval::class, + [ + 'connectionManager' => $this->connectionManager, + 'fieldMapper' => $this->fieldMapper, + 'clientConfig' => $this->clientConfig, + 'searchIndexNameResolver' => $this->searchIndexNameResolver, + 'fieldName' => 'price_0_1', + 'storeId' => 1, + 'entityIds' => [265, 313, 281], + ] + ); + } + + /** + * @dataProvider loadParamsProvider + * @param string $limit + * @param string $offset + * @param string $lower + * @param string $upper + * @param array $queryResult + * @param array $expected + * @return void + */ + public function testLoad( + string $limit, + string $offset, + string $lower, + string $upper, + array $queryResult, + array $expected + ): void { + $this->processQuery($queryResult); + + $this->assertEquals( + $expected, + $this->model->load($limit, $offset, $lower, $upper) + ); + } + + /** + * @dataProvider loadPrevParamsProvider + * @param string $data + * @param string $index + * @param string $lower + * @param array $queryResult + * @param array|bool $expected + * @return void + */ + public function testLoadPrev(string $data, string $index, string $lower, array $queryResult, $expected): void + { + $this->processQuery($queryResult); + + $this->assertEquals( + $expected, + $this->model->loadPrevious($data, $index, $lower) + ); + } + + /** + * @dataProvider loadNextParamsProvider + * @param string $data + * @param string $rightIndex + * @param string $upper + * @param array $queryResult + * @param array|bool $expected + * @return void + */ + public function testLoadNext(string $data, string $rightIndex, string $upper, array $queryResult, $expected): void + { + $this->processQuery($queryResult); + + $this->assertEquals( + $expected, + $this->model->loadNext($data, $rightIndex, $upper) + ); + } + + /** + * @param array $queryResult + * @return void + */ + private function processQuery(array $queryResult): void + { + $this->searchIndexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('magento2_product_1'); + $this->clientConfig->expects($this->any()) + ->method('getEntityType') + ->willReturn('document'); + $this->clientMock->expects($this->any()) + ->method('query') + ->willReturn($queryResult); + } + + /** + * @return array + */ + public function loadParamsProvider(): array + { + return [ + [ + 'limit' => '6', + 'offset' => '2', + 'lower' => '24', + 'upper' => '42', + 'queryResult' => [ + 'hits' => [ + 'hits' => [ + [ + 'fields' => [ + 'price_0_1' => [25], + ], + ], + ], + ], + ], + 'expected' => [25], + ], + ]; + } + + /** + * @return array + */ + public function loadPrevParamsProvider(): array + { + return [ + [ + 'data' => '24', + 'rightIndex' => '1', + 'upper' => '24', + 'queryResult' => [ + 'hits' => [ + 'total'=> '1', + 'hits' => [ + [ + 'fields' => [ + 'price_0_1' => ['25'], + ], + ], + ], + ], + ], + 'expected' => ['25.0'], + ], + [ + 'data' => '24', + 'rightIndex' => '1', + 'upper' => '24', + 'queryResult' => [ + 'hits' => ['total'=> '0'], + ], + 'expected' => false, + ], + ]; + } + + /** + * @return array + */ + public function loadNextParamsProvider(): array + { + return [ + [ + 'data' => '24', + 'rightIndex' => '2', + 'upper' => '42', + 'queryResult' => [ + 'hits' => [ + 'total'=> '1', + 'hits' => [ + [ + 'fields' => [ + 'price_0_1' => ['25'], + ], + ], + ], + ], + ], + 'expected' => ['25.0'], + ], + [ + 'data' => '24', + 'rightIndex' => '2', + 'upper' => '42', + 'queryResult' => [ + 'hits' => ['total'=> '0'], + ], + 'expected' => false, + ], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 2d569eecfee5..0cfaba845fd6 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -166,7 +166,7 @@ Magento\Elasticsearch\SearchAdapter\Aggregation\Interval - Magento\Elasticsearch\SearchAdapter\Aggregation\Interval + Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval diff --git a/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php b/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php new file mode 100644 index 000000000000..aae30026b2b7 --- /dev/null +++ b/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php @@ -0,0 +1,114 @@ +moduleDataSetup = $moduleDataSetup; + $this->structure = $structure; + $this->encryptor = $encryptor; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function apply() + { + $this->moduleDataSetup->startSetup(); + + $this->reEncryptSystemConfigurationValues(); + + $this->moduleDataSetup->endSetup(); + } + + /** + * {@inheritdoc} + */ + public static function getDependencies() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function getAliases() + { + return []; + } + + private function reEncryptSystemConfigurationValues() + { + $structure = $this->structure; + $paths = $this->state->emulateAreaCode( + \Magento\Framework\App\Area::AREA_ADMINHTML, + function () use ($structure) { + return $structure->getFieldPathsByAttribute( + 'backend_model', + \Magento\Config\Model\Config\Backend\Encrypted::class + ); + } + ); + // walk through found data and re-encrypt it + if ($paths) { + $table = $this->moduleDataSetup->getTable('core_config_data'); + $values = $this->moduleDataSetup->getConnection()->fetchPairs( + $this->moduleDataSetup->getConnection() + ->select() + ->from($table, ['config_id', 'value']) + ->where('path IN (?)', $paths) + ->where('value NOT LIKE ?', '') + ); + foreach ($values as $configId => $value) { + $this->moduleDataSetup->getConnection()->update( + $table, + ['value' => $this->encryptor->encrypt($this->encryptor->decrypt($value))], + ['config_id = ?' => (int)$configId] + ); + } + } + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php index e7883693fbe7..e965e8ad207f 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php @@ -391,6 +391,7 @@ protected function _saveValidatedBunches() $nextRowBackup = []; $maxDataSize = $this->_resourceHelper->getMaxDataSize(); $bunchSize = $this->_importExportData->getBunchSize(); + $skuSet = []; $source->rewind(); $this->_dataSourceModel->cleanBunches(); @@ -407,6 +408,7 @@ protected function _saveValidatedBunches() if ($source->valid()) { try { $rowData = $source->current(); + $skuSet[$rowData['sku']] = true; } catch (\InvalidArgumentException $e) { $this->addRowError($e->getMessage(), $this->_processedRowsCount); $this->_processedRowsCount++; @@ -434,6 +436,8 @@ protected function _saveValidatedBunches() $source->next(); } } + $this->_processedEntitiesCount = count($skuSet); + return $this; } diff --git a/app/code/Magento/Sales/Console/Command/EncryptionPaymentDataUpdateCommand.php b/app/code/Magento/Sales/Console/Command/EncryptionPaymentDataUpdateCommand.php new file mode 100644 index 000000000000..47ef24c542c2 --- /dev/null +++ b/app/code/Magento/Sales/Console/Command/EncryptionPaymentDataUpdateCommand.php @@ -0,0 +1,66 @@ +paymentResource = $paymentResource; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName(self::NAME) + ->setDescription( + 'Re-encrypts encrypted credit card data with latest encryption cipher.' + ); + parent::configure(); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + $this->paymentResource->reEncryptCreditCardNumbers(); + } catch (\Exception $e) { + $output->writeln('' . $e->getMessage() . ''); + return Cli::RETURN_FAILURE; + } + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdate.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdate.php new file mode 100644 index 000000000000..4cbcfe49f4cc --- /dev/null +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdate.php @@ -0,0 +1,65 @@ +paymentResource = $paymentResource; + $this->encryptor = $encryptor; + } + + /** + * Fetch encrypted credit card numbers using legacy ciphers and re-encrypt with latest cipher + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function reEncryptCreditCardNumbers() + { + $connection = $this->paymentResource->getConnection(); + $table = $this->paymentResource->getMainTable(); + $select = $connection->select()->from($table, ['entity_id', 'cc_number_enc']) + ->where( + 'cc_number_enc REGEXP ?', + sprintf(self::LEGACY_PATTERN, \Magento\Framework\Encryption\Encryptor::CIPHER_LATEST) + )->limit(1000); + + while ($attributeValues = $connection->fetchPairs($select)) { + // save new values + foreach ($attributeValues as $valueId => $value) { + $connection->update( + $table, + ['cc_number_enc' => $this->encryptor->encrypt($this->encryptor->decrypt($value))], + ['entity_id = ?' => (int)$valueId, 'cc_number_enc = ?' => (string)$value] + ); + } + } + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 0f69389c2969..ed9e5240576d 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -980,6 +980,13 @@ + + + + Magento\Sales\Console\Command\EncryptionPaymentDataUpdateCommand + + + diff --git a/composer.json b/composer.json index b7435e98695c..0cf49466f870 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "magento/zendframework1": "~1.14.1", "monolog/monolog": "^1.17", "oyejorge/less.php": "~1.7.0", + "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^2.0.0", "php-amqplib/php-amqplib": "~2.7.0", "phpseclib/mcrypt_compat": "1.0.5", diff --git a/composer.lock b/composer.lock index d90750e2547e..014c8eca5570 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "74013a4027763e05b29b127b4c03c752", + "content-hash": "da340950d3c725fcf2e01e73c48683d4", "packages": [ { "name": "braintree/braintree_php", @@ -257,16 +257,16 @@ }, { "name": "composer/composer", - "version": "1.7.1", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "5d9311d4555787c8a57fea15f82471499aedf712" + "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/5d9311d4555787c8a57fea15f82471499aedf712", - "reference": "5d9311d4555787c8a57fea15f82471499aedf712", + "url": "https://api.github.com/repos/composer/composer/zipball/576aab9b5abb2ed11a1c52353a759363216a4ad2", + "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2", "shasum": "" }, "require": { @@ -333,7 +333,7 @@ "dependency", "package" ], - "time": "2018-08-07T07:39:23+00:00" + "time": "2018-08-16T14:57:12+00:00" }, { "name": "composer/semver", @@ -460,16 +460,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "c919dc6c62e221fc6406f861ea13433c0aa24f08" + "reference": "e1809da56ce1bd1b547a752936817341ac244d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/c919dc6c62e221fc6406f861ea13433c0aa24f08", - "reference": "c919dc6c62e221fc6406f861ea13433c0aa24f08", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/e1809da56ce1bd1b547a752936817341ac244d8e", + "reference": "e1809da56ce1bd1b547a752936817341ac244d8e", "shasum": "" }, "require": { @@ -500,7 +500,7 @@ "Xdebug", "performance" ], - "time": "2018-04-11T15:42:36+00:00" + "time": "2018-08-16T10:54:23+00:00" }, { "name": "container-interop/container-interop", @@ -1106,6 +1106,88 @@ ], "time": "2018-07-04T16:31:37+00:00" }, + { + "name": "paragonie/sodium_compat", + "version": "v1.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "7d0549c3947eaea620f4e523f42ab236cf7fd304" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/7d0549c3947eaea620f4e523f42ab236cf7fd304", + "reference": "7d0549c3947eaea620f4e523f42ab236cf7fd304", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "time": "2018-06-06T17:30:29+00:00" + }, { "name": "pelago/emogrifier", "version": "v2.0.0", @@ -2400,16 +2482,16 @@ }, { "name": "zendframework/zend-code", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-code.git", - "reference": "6b1059db5b368db769e4392c6cb6cc139e56640d" + "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-code/zipball/6b1059db5b368db769e4392c6cb6cc139e56640d", - "reference": "6b1059db5b368db769e4392c6cb6cc139e56640d", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/c21db169075c6ec4b342149f446e7b7b724f95eb", + "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb", "shasum": "" }, "require": { @@ -2430,8 +2512,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev", - "dev-develop": "3.3-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -2449,7 +2531,7 @@ "code", "zf2" ], - "time": "2017-10-20T15:21:32+00:00" + "time": "2018-08-13T20:36:59+00:00" }, { "name": "zendframework/zend-config", @@ -4725,16 +4807,16 @@ }, { "name": "consolidation/annotated-command", - "version": "2.8.4", + "version": "2.8.5", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c" + "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/651541a0b68318a2a202bda558a676e5ad92223c", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/1e8ff512072422b850b44aa721b5b303e4a5ebb3", + "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3", "shasum": "" }, "require": { @@ -4773,7 +4855,7 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-05-25T18:04:25+00:00" + "time": "2018-08-18T23:51:49+00:00" }, { "name": "consolidation/config", @@ -4935,16 +5017,16 @@ }, { "name": "consolidation/robo", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f" + "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/ac563abfadf7cb7314b4e152f2b5033a6c255f6f", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", + "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", "shasum": "" }, "require": { @@ -4952,6 +5034,8 @@ "consolidation/config": "^1.0.10", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", + "consolidation/self-update": "^1", + "g1a/composer-test-scenarios": "^2", "grasmash/yaml-expander": "^1.3", "league/container": "^2.2", "php": ">=5.5.0", @@ -4968,7 +5052,6 @@ "codeception/aspect-mock": "^1|^2.1.1", "codeception/base": "^2.3.7", "codeception/verify": "^0.3.2", - "g1a/composer-test-scenarios": "^2", "goaop/framework": "~2.1.2", "goaop/parser-reflection": "^1.1.0", "natxet/cssmin": "3.0.4", @@ -5011,7 +5094,50 @@ } ], "description": "Modern task runner", - "time": "2018-05-27T01:42:53+00:00" + "time": "2018-08-17T18:44:18+00:00" + }, + { + "name": "consolidation/self-update", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/consolidation/self-update.git", + "reference": "adbb784e58cc0836d8522851f7e38ee7ade0d553" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/adbb784e58cc0836d8522851f7e38ee7ade0d553", + "reference": "adbb784e58cc0836d8522851f7e38ee7ade0d553", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "symfony/console": "^2.8|^3|^4", + "symfony/filesystem": "^2.5|^3|^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "SelfUpdate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander Menk", + "email": "menk@mestrona.net" + } + ], + "description": "Provides a self:update command for Symfony Console applications.", + "time": "2018-08-17T04:50:59+00:00" }, { "name": "dflydev/dot-access-data", @@ -5464,21 +5590,21 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.12.2", + "version": "v2.12.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "dcc87d5414e9d0bd316fce81a5bedb9ce720b183" + "reference": "b23d49981cfc95497d03081aeb6df6575196a0d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/dcc87d5414e9d0bd316fce81a5bedb9ce720b183", - "reference": "dcc87d5414e9d0bd316fce81a5bedb9ce720b183", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/b23d49981cfc95497d03081aeb6df6575196a0d3", + "reference": "b23d49981cfc95497d03081aeb6df6575196a0d3", "shasum": "" }, "require": { "composer/semver": "^1.4", - "composer/xdebug-handler": "^1.0", + "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", @@ -5551,7 +5677,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2018-07-06T10:37:40+00:00" + "time": "2018-08-19T22:33:38+00:00" }, { "name": "fzaninotto/faker", @@ -5603,6 +5729,39 @@ ], "time": "2018-07-12T10:23:15+00:00" }, + { + "name": "g1a/composer-test-scenarios", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/g1a/composer-test-scenarios.git", + "reference": "a166fd15191aceab89f30c097e694b7cf3db4880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/g1a/composer-test-scenarios/zipball/a166fd15191aceab89f30c097e694b7cf3db4880", + "reference": "a166fd15191aceab89f30c097e694b7cf3db4880", + "shasum": "" + }, + "bin": [ + "scripts/create-scenario", + "scripts/dependency-licenses", + "scripts/install-scenario" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Greg Anderson", + "email": "greg.1.anderson@greenknowe.org" + } + ], + "description": "Useful scripts for testing multiple sets of Composer dependencies.", + "time": "2018-08-08T23:37:23+00:00" + }, { "name": "grasmash/expander", "version": "1.0.0", diff --git a/dev/tests/functional/tests/app/Magento/Braintree/Test/Constraint/AssertDeviceDataIsPresentInBraintreeRequest.php b/dev/tests/functional/tests/app/Magento/Braintree/Test/Constraint/AssertDeviceDataIsPresentInBraintreeRequest.php index 8320514520b0..b238302e8f47 100644 --- a/dev/tests/functional/tests/app/Magento/Braintree/Test/Constraint/AssertDeviceDataIsPresentInBraintreeRequest.php +++ b/dev/tests/functional/tests/app/Magento/Braintree/Test/Constraint/AssertDeviceDataIsPresentInBraintreeRequest.php @@ -17,7 +17,7 @@ class AssertDeviceDataIsPresentInBraintreeRequest extends AbstractConstraint /** * Log file name. */ - const FILE_NAME = 'debug.log'; + const FILE_NAME = 'payment.log'; /** * Device data pattern for regular expression. diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/Repository/Integration.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/Repository/Integration.xml index abfc9e09cb43..7d4811c4cb55 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/Repository/Integration.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/Repository/Integration.xml @@ -40,6 +40,7 @@ Stores System Global Search + Catalog diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/ActivateIntegrationEntityTest.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/ActivateIntegrationEntityTest.xml index cae8dec6edcf..11a8ed006af3 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/ActivateIntegrationEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/ActivateIntegrationEntityTest.xml @@ -14,7 +14,6 @@ - MAGETWO-66745: Magento\Integration\Test\TestCase\ActivateIntegrationEntityTest with ActivateIntegrationEntityTestVariation1 fails randomly diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AbstractAssertWishlistProductDetails.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AbstractAssertWishlistProductDetails.php index c00651638627..803bee65fcdf 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AbstractAssertWishlistProductDetails.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AbstractAssertWishlistProductDetails.php @@ -16,6 +16,11 @@ */ abstract class AbstractAssertWishlistProductDetails extends AbstractAssertForm { + /** + * @inheritdoc + */ + protected $skippedFields = ['sku']; + /** * Assert product details. * diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml index fd80d385e485..95e6a854ed26 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/MoveProductFromShoppingCartToWishlistTest.xml @@ -37,7 +37,6 @@ - stable:no bundleProduct::bundle_dynamic_product @@ -45,7 +44,6 @@ - stable:no bundleProduct::bundle_fixed_product diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Model/ResourceModel/Key/ChangeTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Model/ResourceModel/Key/ChangeTest.php index c199214f6857..496d03dbd7c5 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Model/ResourceModel/Key/ChangeTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Model/ResourceModel/Key/ChangeTest.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\EncryptionKey\Model\ResourceModel\Key; class ChangeTest extends \PHPUnit\Framework\TestCase @@ -79,7 +81,7 @@ public function testChangeEncryptionKey() ) ); $this->assertNotContains($testValue, $values1); - $this->assertRegExp('|([0-9]+:)([0-9]+:)([a-zA-Z0-9]+:)([a-zA-Z0-9+/]+=)|', current($values1)); + $this->assertRegExp('|([0-9]+:)([0-9]+:)([a-zA-Z0-9+/]+=*)|', current($values1)); // Verify that the credit card number has been encrypted $values2 = $connection->fetchPairs( @@ -89,7 +91,7 @@ public function testChangeEncryptionKey() ) ); $this->assertNotContains('1111111111', $values2); - $this->assertRegExp('|([0-9]+:)([0-9]+:)([a-zA-Z0-9]+:)([a-zA-Z0-9+/]+=)|', current($values1)); + $this->assertRegExp('|([0-9]+:)([0-9]+:)([a-zA-Z0-9+/]+=*)|', current($values2)); /** clean up */ $select = $connection->select()->from($configModel->getMainTable())->where('path=?', $testPath); diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php new file mode 100644 index 000000000000..3a47692bdb93 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php @@ -0,0 +1,98 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->deployConfig = $this->objectManager->get(DeploymentConfig::class); + } + + public function testChangeEncryptionKey() + { + $testPath = 'test/config'; + $testValue = 'test'; + + $structureMock = $this->createMock(\Magento\Config\Model\Config\Structure\Proxy::class); + $structureMock->expects($this->once()) + ->method('getFieldPathsByAttribute') + ->will($this->returnValue([$testPath])); + + /** @var \Magento\Config\Model\ResourceModel\Config $configModel */ + $configModel = $this->objectManager->create(\Magento\Config\Model\ResourceModel\Config::class); + $configModel->saveConfig($testPath, $this->legacyEncrypt($testValue), 'default', 0); + + /** @var \Magento\EncryptionKey\Setup\Patch\Data\SodiumChachaPatch $patch */ + $patch = $this->objectManager->create( + \Magento\EncryptionKey\Setup\Patch\Data\SodiumChachaPatch::class, + [ + 'structure' => $structureMock, + ] + ); + $patch->apply(); + + $connection = $configModel->getConnection(); + $values = $connection->fetchPairs( + $connection->select()->from( + $configModel->getMainTable(), + ['config_id', 'value'] + )->where( + 'path IN (?)', + [$testPath] + )->where( + 'value NOT LIKE ?', + '' + ) + ); + + /** @var \Magento\Framework\Encryption\EncryptorInterface $encyptor */ + $encyptor = $this->objectManager->get(\Magento\Framework\Encryption\EncryptorInterface::class); + + $rawConfigValue = array_pop($values); + + $this->assertNotEquals($testValue, $rawConfigValue); + $this->assertStringStartsWith('0:' . Encryptor::CIPHER_LATEST . ':', $rawConfigValue); + $this->assertEquals($testValue, $encyptor->decrypt($rawConfigValue)); + } + + private function legacyEncrypt(string $data): string + { + // @codingStandardsIgnoreStart + $handle = @mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, ''); + $initVectorSize = @mcrypt_enc_get_iv_size($handle); + $initVector = str_repeat("\0", $initVectorSize); + @mcrypt_generic_init($handle, $this->deployConfig->get(static::PATH_KEY), $initVector); + + $encrpted = @mcrypt_generic($handle, $data); + + @mcrypt_generic_deinit($handle); + @mcrypt_module_close($handle); + // @codingStandardsIgnoreEnd + + return '0:' . Encryptor::CIPHER_RIJNDAEL_256 . ':' . base64_encode($encrpted); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Encryption/ModelTest.php b/dev/tests/integration/testsuite/Magento/Framework/Encryption/ModelTest.php index f5b48e79b986..12f398b705b2 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Encryption/ModelTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Encryption/ModelTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Encryption; class ModelTest extends \PHPUnit\Framework\TestCase @@ -40,7 +42,16 @@ public function testEncryptDecrypt2() public function testValidateKey() { $validKey = md5(uniqid()); - $this->assertInstanceOf(\Magento\Framework\Encryption\Crypt::class, $this->_model->validateKey($validKey)); + $this->_model->validateKey($validKey); + } + + /** + * @expectedException \Exception + */ + public function testValidateKeyInvalid() + { + $invalidKey = '---- '; + $this->_model->validateKey($invalidKey); } public function testGetValidateHash() diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/EntityAbstractTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/EntityAbstractTest.php new file mode 100644 index 000000000000..db4d9c546864 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/EntityAbstractTest.php @@ -0,0 +1,68 @@ +create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = new Csv(__DIR__ . '/_files/advanced_price_for_validation_test.csv', $directory); + $source->rewind(); + + $eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); + $entityTypeMock = $this->createMock(\Magento\Eav\Model\Entity\Type::class); + $eavConfig->expects($this->any())->method('getEntityType')->willReturn($entityTypeMock); + + /** @var $model AbstractEntity|\PHPUnit_Framework_MockObject_MockObject */ + $model = $this->getMockForAbstractClass( + AbstractEntity::class, + [ + $objectManager->get(\Magento\Framework\Json\Helper\Data::class), + $objectManager->get(\Magento\ImportExport\Helper\Data::class), + $objectManager->get(\Magento\ImportExport\Model\ResourceModel\Import\Data::class), + $eavConfig, + $objectManager->get(\Magento\Framework\App\ResourceConnection::class), + $objectManager->get(\Magento\ImportExport\Model\ResourceModel\Helper::class), + $objectManager->get(\Magento\Framework\Stdlib\StringUtils::class), + $objectManager->get(ProcessingErrorAggregatorInterface::class), + ], + '', + true, + false, + true, + ['validateRow', 'getEntityTypeCode'] + ); + $model->expects($this->any())->method('validateRow')->willReturn(true); + $model->expects($this->any())->method('getEntityTypeCode')->willReturn('catalog_product'); + + $model->setSource($source); + + $method = new \ReflectionMethod($model, '_saveValidatedBunches'); + $method->setAccessible(true); + $method->invoke($model); + + $this->assertEquals(1, $model->getProcessedEntitiesCount()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/_files/advanced_price_for_validation_test.csv b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/_files/advanced_price_for_validation_test.csv new file mode 100644 index 000000000000..fb42afb90188 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Import/Entity/_files/advanced_price_for_validation_test.csv @@ -0,0 +1,3 @@ +sku,tier_price_website,tier_price_customer_group,tier_price_qty,tier_price,tier_price_value_type +SimpleProduct,All Websites [USD],ALL GROUPS,100,50,Fixed +SimpleProduct,All Websites [USD],ALL GROUPS,33,55,Fixed diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdateTest.php new file mode 100644 index 000000000000..c0de639ea742 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Payment/EncryptionUpdateTest.php @@ -0,0 +1,50 @@ +get(\Magento\Framework\Encryption\EncryptorInterface::class); + + /** @var \Magento\Sales\Model\ResourceModel\Order\Payment\EncryptionUpdate $resource */ + $resource = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Payment\EncryptionUpdate::class); + $resource->reEncryptCreditCardNumbers(); + + /** @var \Magento\Sales\Model\ResourceModel\Order\Payment\Collection $collection */ + $collection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Payment\Collection::class); + $collection->addFieldToFilter('cc_number_enc', ['notnull' => true]); + + $this->assertGreaterThan(0, $collection->getTotalCount()); + + /** @var \Magento\Sales\Model\Order\Payment $payment */ + foreach ($collection->getItems() as $payment) { + $this->assertEquals( + static::TEST_CC_NUMBER, + $encyptor->decrypt($payment->getCcNumberEnc()) + ); + + $this->assertStringStartsWith('0:' . Encryptor::CIPHER_LATEST . ':', $payment->getCcNumberEnc()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php b/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php new file mode 100644 index 000000000000..bfa643fcf5f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php @@ -0,0 +1,53 @@ +get(DeploymentConfig::class); + +/** + * Creates an encrypted card number with the current crypt key using + * a legacy cipher. + */ +// @codingStandardsIgnoreStart +$handle = @mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, ''); +$initVectorSize = @mcrypt_enc_get_iv_size($handle); +$initVector = str_repeat("\0", $initVectorSize); +@mcrypt_generic_init($handle, $deployConfig->get('crypt/key'), $initVector); + +$encCcNumber = @mcrypt_generic($handle, EncryptionUpdateTest::TEST_CC_NUMBER); + +@mcrypt_generic_deinit($handle); +@mcrypt_module_close($handle); +// @codingStandardsIgnoreEnd + +/** @var SearchCriteria $searchCriteria */ +$searchCriteria = $objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', '100000001') + ->create(); + +$orders = $orderRepository->getList($searchCriteria)->getItems(); +$order = array_pop($orders); + +/** @var \Magento\Sales\Model\ResourceModel\Order\Payment $resource */ +$resource = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Payment::class); +$resource->getConnection()->insert( + $resource->getMainTable(), + [ + 'parent_id' => $order->getId(), + 'cc_number_enc' => '0:2:' . base64_encode($encCcNumber), + ] +); diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/blacklist/obsolete_mage.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/blacklist/obsolete_mage.php index 0f7231e19aee..62816cd4e4f7 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/blacklist/obsolete_mage.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/blacklist/obsolete_mage.php @@ -10,4 +10,6 @@ 'dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php', 'lib/internal/Magento/Framework/ObjectManager/Test/Unit/Factory/CompiledTest.php', 'dev/tests/integration/testsuite/Magento/Indexer/Model/Config/_files/result.php', + 'lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php', + 'lib/internal/Magento/Framework/Encryption/Test/Unit/CryptTest.php' ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php index 78ab26401b0f..12c10990a3af 100755 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php @@ -4237,4 +4237,5 @@ ['Zend_Feed', 'Zend\Feed'], ['Zend_Uri', 'Zend\Uri\Uri'], ['Zend_Mime', 'Magento\Framework\HTTP\Mime'], + ['Magento\Framework\Encryption\Crypt', 'Magento\Framework\Encryption\EncryptionAdapterInterface'], ]; diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 52e7137fa3dd..92f0302d93ba 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Config; /** @@ -127,5 +129,5 @@ class ConfigOptionsListConstants /** * Size of random string generated for store's encryption key */ - const STORE_KEY_RANDOM_STRING_SIZE = 32; + const STORE_KEY_RANDOM_STRING_SIZE = SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES; } diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/EncryptionAdapterInterface.php b/lib/internal/Magento/Framework/Encryption/Adapter/EncryptionAdapterInterface.php new file mode 100644 index 000000000000..b9bbb089ae0c --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Adapter/EncryptionAdapterInterface.php @@ -0,0 +1,24 @@ +cipher = $cipher; + $this->mode = $mode; + // @codingStandardsIgnoreLine + $this->handle = @mcrypt_module_open($cipher, '', $mode, ''); + try { + // @codingStandardsIgnoreLine + $maxKeySize = @mcrypt_enc_get_key_size($this->handle); + if (strlen($key) > $maxKeySize) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase('Key must not exceed %1 bytes.', [$maxKeySize]) + ); + } + // @codingStandardsIgnoreLine + $initVectorSize = @mcrypt_enc_get_iv_size($this->handle); + if (null === $initVector) { + /* Set vector to zero bytes to not use it */ + $initVector = str_repeat("\0", $initVectorSize); + } elseif (!is_string($initVector) || strlen($initVector) != $initVectorSize) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase( + 'Init vector must be a string of %1 bytes.', + [$initVectorSize] + ) + ); + } + $this->initVector = $initVector; + } catch (\Exception $e) { + // @codingStandardsIgnoreLine + @mcrypt_module_close($this->handle); + throw new \Magento\Framework\Exception\LocalizedException(new \Magento\Framework\Phrase($e->getMessage())); + } + // @codingStandardsIgnoreLine + @mcrypt_generic_init($this->handle, $key, $initVector); + } + + /** + * Destructor frees allocated resources + */ + public function __destruct() + { + // @codingStandardsIgnoreStart + @mcrypt_generic_deinit($this->handle); + @mcrypt_module_close($this->handle); + // @codingStandardsIgnoreEnd + } + + /** + * Retrieve a name of currently used cryptographic algorithm + * + * @return string + */ + public function getCipher(): string + { + return $this->cipher; + } + + /** + * Mode in which cryptographic algorithm is running + * + * @return string + */ + public function getMode(): string + { + return $this->mode; + } + + /** + * Retrieve an actual value of initial vector that has been used to initialize a cipher + * + * @return string + */ + public function getInitVector(): ?string + { + return $this->initVector; + } + + /** + * Get the current mcrypt handle + * + * @return resource + */ + public function getHandle() + { + return $this->handle; + } + + /** + * Encrypt a string + * + * @param string $data String to encrypt + * @return string + * @throws \Exception + */ + public function encrypt(string $data): string + { + throw new \Exception( + (string)new \Magento\Framework\Phrase('Mcrypt cannot be used for encryption. Use Sodium instead') + ); + } + + /** + * Decrypt a string + * + * @param string $data + * @return string + */ + public function decrypt(string $data): string + { + if (strlen($data) == 0) { + return $data; + } + // @codingStandardsIgnoreLine + $data = @mdecrypt_generic($this->handle, $data); + /* + * Returned string can in fact be longer than the unencrypted string due to the padding of the data + * @link http://www.php.net/manual/en/function.mdecrypt-generic.php + */ + $data = rtrim($data, "\0"); + return $data; + } +} diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php new file mode 100644 index 000000000000..9f9facf98ff8 --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php @@ -0,0 +1,70 @@ +key = $key; + } + + /** + * Encrypt a string + * + * @param string $data + * @return string string + */ + public function encrypt(string $data): string + { + $nonce = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES); + $cipherText = sodium_crypto_aead_chacha20poly1305_ietf_encrypt( + (string)$data, + $nonce, + $nonce, + $this->key + ); + + return $nonce . $cipherText; + } + + /** + * Decrypt a string + * + * @param string $data + * @return string + */ + public function decrypt(string $data): string + { + $nonce = mb_substr($data, 0, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, '8bit'); + $payload = mb_substr($data, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, null, '8bit'); + + $plainText = sodium_crypto_aead_chacha20poly1305_ietf_decrypt( + $payload, + $nonce, + $nonce, + $this->key + ); + + return $plainText; + } +} diff --git a/lib/internal/Magento/Framework/Encryption/Crypt.php b/lib/internal/Magento/Framework/Encryption/Crypt.php index 5f368f0d5b79..d52db40b395a 100644 --- a/lib/internal/Magento/Framework/Encryption/Crypt.php +++ b/lib/internal/Magento/Framework/Encryption/Crypt.php @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Encryption; /** * Class encapsulates cryptographic algorithm * * @api + * @deprecated */ class Crypt { @@ -29,11 +32,11 @@ class Crypt protected $_initVector; /** - * Encryption algorithm module handle + * Mcrypt adapter * - * @var resource + * @var \Magento\Framework\Encryption\Adapter\Mcrypt */ - protected $_handle; + private $mcrypt; /** * Constructor @@ -53,64 +56,30 @@ public function __construct( $mode = MCRYPT_MODE_ECB, $initVector = false ) { - $this->_cipher = $cipher; - $this->_mode = $mode; - // @codingStandardsIgnoreStart - $this->_handle = @mcrypt_module_open($cipher, '', $mode, ''); - // @codingStandardsIgnoreEnd - try { - // @codingStandardsIgnoreStart - $maxKeySize = @mcrypt_enc_get_key_size($this->_handle); - // @codingStandardsIgnoreEnd - if (strlen($key) > $maxKeySize) { - throw new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase('Key must not exceed %1 bytes.', [$maxKeySize]) - ); - } + if (true === $initVector) { // @codingStandardsIgnoreStart - $initVectorSize = @mcrypt_enc_get_iv_size($this->_handle); + $handle = @mcrypt_module_open($cipher, '', $mode, ''); + $initVectorSize = @mcrypt_enc_get_iv_size($handle); // @codingStandardsIgnoreEnd - if (true === $initVector) { - /* Generate a random vector from human-readable characters */ - $abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - $initVector = ''; - for ($i = 0; $i < $initVectorSize; $i++) { - $initVector .= $abc[random_int(0, strlen($abc) - 1)]; - } - } elseif (false === $initVector) { - /* Set vector to zero bytes to not use it */ - $initVector = str_repeat("\0", $initVectorSize); - } elseif (!is_string($initVector) || strlen($initVector) != $initVectorSize) { - throw new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase( - 'Init vector must be a string of %1 bytes.', - [$initVectorSize] - ) - ); + + /* Generate a random vector from human-readable characters */ + $allowedCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $initVector = ''; + for ($i = 0; $i < $initVectorSize; $i++) { + $initVector .= $allowedCharacters[rand(0, strlen($allowedCharacters) - 1)]; } - $this->_initVector = $initVector; - } catch (\Exception $e) { // @codingStandardsIgnoreStart - @mcrypt_module_close($this->_handle); + @mcrypt_generic_deinit($handle); + @mcrypt_module_close($handle); // @codingStandardsIgnoreEnd - throw $e; } - // @codingStandardsIgnoreStart - @mcrypt_generic_init($this->_handle, $key, $initVector); - // @codingStandardsIgnoreEnd - } - /** - * Destructor frees allocated resources - */ - public function __destruct() - { - // @codingStandardsIgnoreStart - @mcrypt_generic_deinit($this->_handle); - // @codingStandardsIgnoreEnd - // @codingStandardsIgnoreStart - @mcrypt_module_close($this->_handle); - // @codingStandardsIgnoreEnd + $this->mcrypt = new \Magento\Framework\Encryption\Adapter\Mcrypt( + $key, + $cipher, + $mode, + $initVector === false ? null : $initVector + ); } /** @@ -120,7 +89,7 @@ public function __destruct() */ public function getCipher() { - return $this->_cipher; + return $this->mcrypt->getCipher(); } /** @@ -130,7 +99,7 @@ public function getCipher() */ public function getMode() { - return $this->_mode; + return $this->mcrypt->getMode(); } /** @@ -140,7 +109,7 @@ public function getMode() */ public function getInitVector() { - return $this->_initVector; + return $this->mcrypt->getInitVector(); } /** @@ -154,9 +123,8 @@ public function encrypt($data) if (strlen($data) == 0) { return $data; } - // @codingStandardsIgnoreStart - return @mcrypt_generic($this->_handle, $data); - // @codingStandardsIgnoreEnd + // @codingStandardsIgnoreLine + return @mcrypt_generic($this->mcrypt->getHandle(), $data); } /** @@ -167,17 +135,6 @@ public function encrypt($data) */ public function decrypt($data) { - if (strlen($data) == 0) { - return $data; - } - // @codingStandardsIgnoreStart - $data = @mdecrypt_generic($this->_handle, $data); - // @codingStandardsIgnoreEnd - /* - * Returned string can in fact be longer than the unencrypted string due to the padding of the data - * @link http://www.php.net/manual/en/function.mdecrypt-generic.php - */ - $data = rtrim($data, "\0"); - return $data; + return $this->mcrypt->decrypt($data); } } diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index 881c5843b155..f7d52d5474ad 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Encryption; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Encryption\Adapter\EncryptionAdapterInterface; use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Math\Random; +use Magento\Framework\Encryption\Adapter\SodiumChachaIetf; +use Magento\Framework\Encryption\Adapter\Mcrypt; /** * Class Encryptor provides basic logic for hashing strings and encrypting/decrypting misc data @@ -56,7 +62,9 @@ class Encryptor implements EncryptorInterface const CIPHER_RIJNDAEL_256 = 2; - const CIPHER_LATEST = 2; + const CIPHER_AEAD_CHACHA20POLY1305 = 3; + + const CIPHER_LATEST = 3; /**#@-*/ /** @@ -108,6 +116,7 @@ class Encryptor implements EncryptorInterface private $random; /** + * Encryptor constructor. * @param Random $random * @param DeploymentConfig $deploymentConfig */ @@ -118,7 +127,7 @@ public function __construct( $this->random = $random; // load all possible keys - $this->keys = preg_split('/\s+/s', trim($deploymentConfig->get(self::PARAM_CRYPT_KEY))); + $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get(self::PARAM_CRYPT_KEY))); $this->keyVersion = count($this->keys) - 1; } @@ -133,7 +142,12 @@ public function __construct( */ public function validateCipher($version) { - $types = [self::CIPHER_BLOWFISH, self::CIPHER_RIJNDAEL_128, self::CIPHER_RIJNDAEL_256]; + $types = [ + self::CIPHER_BLOWFISH, + self::CIPHER_RIJNDAEL_128, + self::CIPHER_RIJNDAEL_256, + self::CIPHER_AEAD_CHACHA20POLY1305, + ]; $version = (int)$version; if (!in_array($version, $types, true)) { @@ -172,7 +186,7 @@ public function getHash($password, $salt = false, $version = self::HASH_VERSION_ */ public function hash($data, $version = self::HASH_VERSION_LATEST) { - return hash($this->hashVersionMap[$version], $data); + return hash($this->hashVersionMap[$version], (string)$data); } /** @@ -260,14 +274,11 @@ private function getPasswordVersion() */ public function encrypt($data) { - $crypt = $this->getCrypt(); - if (null === $crypt) { - return $data; - } - return $this->keyVersion . ':' . $this->cipher . ':' . (MCRYPT_MODE_CBC === - $crypt->getMode() ? $crypt->getInitVector() . ':' : '') . base64_encode( - $crypt->encrypt((string)$data) - ); + $crypt = new SodiumChachaIetf($this->keys[$this->keyVersion]); + + return $this->keyVersion . + ':' . self::CIPHER_AEAD_CHACHA20POLY1305 . + ':' . base64_encode($crypt->encrypt($data)); } /** @@ -279,6 +290,7 @@ public function encrypt($data) * * @param string $data * @return string + * @throws \Exception */ public function decrypt($data) { @@ -286,11 +298,11 @@ public function decrypt($data) $parts = explode(':', $data, 4); $partsCount = count($parts); - $initVector = false; + $initVector = null; // specified key, specified crypt, specified iv if (4 === $partsCount) { list($keyVersion, $cryptVersion, $iv, $data) = $parts; - $initVector = $iv ? $iv : false; + $initVector = $iv ? $iv : null; $keyVersion = (int)$keyVersion; $cryptVersion = self::CIPHER_RIJNDAEL_256; // specified key, specified crypt @@ -325,10 +337,9 @@ public function decrypt($data) } /** - * Return crypt model, instantiate if it is empty + * Validate key contains only allowed characters * * @param string|null $key NULL value means usage of the default key specified on constructor - * @return \Magento\Framework\Encryption\Crypt * @throws \Exception */ public function validateKey($key) @@ -336,7 +347,6 @@ public function validateKey($key) if (preg_match('/\s/s', $key)) { throw new \Exception((string)new \Magento\Framework\Phrase('The encryption key format is invalid.')); } - return $this->getCrypt($key); } /** @@ -344,6 +354,7 @@ public function validateKey($key) * * @param string $key * @return $this + * @throws \Exception */ public function setNewKey($key) { @@ -370,11 +381,15 @@ public function exportKeys() * * @param string $key * @param int $cipherVersion - * @param bool $initVector - * @return Crypt|null + * @param string $initVector + * @return EncryptionAdapterInterface|null + * @throws \Exception */ - protected function getCrypt($key = null, $cipherVersion = null, $initVector = true) - { + private function getCrypt( + string $key = null, + int $cipherVersion = null, + string $initVector = null + ): ?EncryptionAdapterInterface { if (null === $key && null === $cipherVersion) { $cipherVersion = self::CIPHER_RIJNDAEL_256; } @@ -392,6 +407,10 @@ protected function getCrypt($key = null, $cipherVersion = null, $initVector = tr } $cipherVersion = $this->validateCipher($cipherVersion); + if ($cipherVersion >= self::CIPHER_AEAD_CHACHA20POLY1305) { + return new SodiumChachaIetf($key); + } + if ($cipherVersion === self::CIPHER_RIJNDAEL_128) { $cipher = MCRYPT_RIJNDAEL_128; $mode = MCRYPT_MODE_ECB; @@ -403,6 +422,6 @@ protected function getCrypt($key = null, $cipherVersion = null, $initVector = tr $mode = MCRYPT_MODE_ECB; } - return new Crypt($key, $cipher, $mode, $initVector); + return new Mcrypt($key, $cipher, $mode, $initVector); } } diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php new file mode 100644 index 000000000000..2622441aa089 --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/McryptTest.php @@ -0,0 +1,188 @@ + [MCRYPT_MODE_ECB], + MCRYPT_RIJNDAEL_128 => [MCRYPT_MODE_ECB], + MCRYPT_RIJNDAEL_256 => [MCRYPT_MODE_CBC], + ]; + + protected function setUp() + { + $this->key = substr(__CLASS__, -32, 32); + } + + protected function getRandomString(int $length): string + { + $result = ''; + + do { + $result .= sha1(microtime()); + } while (strlen($result) < $length); + + return substr($result, -$length); + } + + private function requireCipherInfo() + { + $filename = __DIR__ . '/../Crypt/_files/_cipher_info.php'; + + if (!self::$cipherInfo) { + self::$cipherInfo = include $filename; + } + } + + private function getKeySize(string $cipherName, string $modeName): int + { + $this->requireCipherInfo(); + return self::$cipherInfo[$cipherName][$modeName]['key_size']; + } + + private function getInitVectorSize(string $cipherName, string $modeName): int + { + $this->requireCipherInfo(); + return self::$cipherInfo[$cipherName][$modeName]['iv_size']; + } + + public function getCipherModeCombinations(): array + { + $result = []; + foreach (self::SUPPORTED_CIPHER_MODE_COMBINATIONS as $cipher => $modes) { + /** @var array $modes */ + foreach ($modes as $mode) { + $result[$cipher . '-' . $mode] = [$cipher, $mode]; + } + } + return $result; + } + + /** + * @dataProvider getCipherModeCombinations + */ + public function testConstructor(string $cipher, string $mode) + { + /* Generate random init vector */ + $initVector = $this->getRandomString($this->getInitVectorSize($cipher, $mode)); + + $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt($this->key, $cipher, $mode, $initVector); + + $this->assertEquals($cipher, $crypt->getCipher()); + $this->assertEquals($mode, $crypt->getMode()); + $this->assertEquals($initVector, $crypt->getInitVector()); + } + + public function getConstructorExceptionData(): array + { + $key = substr(__CLASS__, -32, 32); + $result = []; + foreach (self::SUPPORTED_CIPHER_MODE_COMBINATIONS as $cipher => $modes) { + /** @var array $modes */ + foreach ($modes as $mode) { + $tooLongKey = str_repeat('-', $this->getKeySize($cipher, $mode) + 1); + $tooShortInitVector = str_repeat('-', $this->getInitVectorSize($cipher, $mode) - 1); + $tooLongInitVector = str_repeat('-', $this->getInitVectorSize($cipher, $mode) + 1); + $result['tooLongKey-' . $cipher . '-' . $mode . '-false'] = [$tooLongKey, $cipher, $mode, false]; + $keyPrefix = 'key-' . $cipher . '-' . $mode; + $result[$keyPrefix . '-tooShortInitVector'] = [$key, $cipher, $mode, $tooShortInitVector]; + $result[$keyPrefix . '-tooLongInitVector'] = [$key, $cipher, $mode, $tooLongInitVector]; + } + } + return $result; + } + + /** + * @dataProvider getConstructorExceptionData + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testConstructorException(string $key, string $cipher, string $mode, ?string $initVector = null) + { + new \Magento\Framework\Encryption\Adapter\Mcrypt($key, $cipher, $mode, $initVector); + } + + public function testConstructorDefaults() + { + $cryptExpected = new \Magento\Framework\Encryption\Adapter\Mcrypt( + $this->key, + MCRYPT_BLOWFISH, + MCRYPT_MODE_ECB, + null + ); + $cryptActual = new \Magento\Framework\Encryption\Adapter\Mcrypt($this->key); + + $this->assertEquals($cryptExpected->getCipher(), $cryptActual->getCipher()); + $this->assertEquals($cryptExpected->getMode(), $cryptActual->getMode()); + $this->assertEquals($cryptExpected->getInitVector(), $cryptActual->getInitVector()); + } + + public function getCryptData(): array + { + $fixturesFilename = __DIR__ . '/../Crypt/_files/_crypt_fixtures.php'; + + $result = include $fixturesFilename; + /* Restore encoded string back to binary */ + foreach ($result as &$cryptParams) { + $cryptParams[5] = base64_decode($cryptParams[5]); + } + unset($cryptParams); + + return $result; + } + + /** + * @dataProvider getCryptData + */ + public function testDecrypt( + string $key, + string $cipher, + string $mode, + ?string $initVector, + string $expectedData, + string $inputData + ) { + $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt($key, $cipher, $mode, $initVector); + $actualData = $crypt->decrypt($inputData); + $this->assertEquals($expectedData, $actualData); + } + + /** + * @expectedException \Exception + */ + public function testEncrypt() + { + $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt($this->key); + $crypt->encrypt('Hello World!!'); + } + + /** + * @dataProvider getCipherModeCombinations + */ + public function testInitVectorNone(string $cipher, string $mode) + { + $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt( + $this->key, + $cipher, + $mode, + null + ); + $actualInitVector = $crypt->getInitVector(); + + $expectedInitVector = str_repeat("\0", $this->getInitVectorSize($cipher, $mode)); + $this->assertEquals($expectedInitVector, $actualInitVector); + } +} diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php new file mode 100644 index 000000000000..8092ce55b42c --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Adapter/SodiumChachaIetfTest.php @@ -0,0 +1,51 @@ +encrypt($decrypted); + + $this->assertNotEquals($encrypted, $result); + } + + /** + * @dataProvider getCryptData + */ + public function testDecrypt(string $key, string $encrypted, string $decrypted) + { + $crypt = new \Magento\Framework\Encryption\Adapter\SodiumChachaIetf($key); + $result = $crypt->decrypt($encrypted); + + $this->assertEquals($decrypted, $result); + } +} diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php new file mode 100644 index 000000000000..7917bd9ba83e --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/Crypt/_files/_sodium_chachaieft_fixtures.php @@ -0,0 +1,35 @@ + [ + 'key' => '6wRADHwwCBGgdxbcHhovGB0upmg0mbsN', + 'encrypted' => '146BhsQ3grT0VgkYuY3ii3gpClXHkFqlIcNpAD4+bAMBP+ToCHZHiJID', + 'decrypted' => 'Hello World!!!', + ], + 1 => [ + 'key' => 'uPuzBU067DXTM4PqEi14Sv5tbWjVcRZI', + 'encrypted' => '6SQaVrCnY10n8tOxYyvWuVGKddjR12ZbGylM9K+bRHqsqltRwuLs15vV', + 'decrypted' => 'Hello World!!!', + ], + 2 => [ + 'key' => 'zsmVdKkwVgylxMM8ZzQ3GTv7SxvusKnJ', + 'encrypted' => 'eQcREUJDV8EEB9WA1pBd5LbVQrs4Kyv6iWnkhOnjeitySuPQAcpIVoCM', + 'decrypted' => 'Hello World!!!', + ], + 3 => [ + 'key' => 'aggaHLvRCxRRyebpsrGAdLAIfSrufYrN', + 'encrypted' => 'PSOa8KCpTsxnTgq4IKbpneF38FIp0JeAeiXQIf30vS5X+riylx05pz9b', + 'decrypted' => 'Hello World!!!', + ], + 4 => [ + 'key' => '6tEWnKY6AcdjS2XfPe1DjTbkvu2cFFZo', + 'encrypted' => 'UglO9dEgslFpwPwejJmrK89PmBicv+I1pfdaXaEI69IrETD8LpdzOLF7', + 'decrypted' => 'Hello World!!!', + ], +]; diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/CryptTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/CryptTest.php index a37dc981518e..c286cb0a1d80 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/CryptTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/CryptTest.php @@ -105,6 +105,7 @@ public function testConstructor($cipher, $mode) */ public function getConstructorExceptionData() { + $key = substr(__CLASS__, -32, 32); $result = []; foreach (self::SUPPORTED_CIPHER_MODE_COMBINATIONS as $cipher => $modes) { /** @var array $modes */ @@ -114,8 +115,8 @@ public function getConstructorExceptionData() $tooLongInitVector = str_repeat('-', $this->_getInitVectorSize($cipher, $mode) + 1); $result['tooLongKey-' . $cipher . '-' . $mode . '-false'] = [$tooLongKey, $cipher, $mode, false]; $keyPrefix = 'key-' . $cipher . '-' . $mode; - $result[$keyPrefix . '-tooShortInitVector'] = [$this->_key, $cipher, $mode, $tooShortInitVector]; - $result[$keyPrefix . '-tooLongInitVector'] = [$this->_key, $cipher, $mode, $tooLongInitVector]; + $result[$keyPrefix . '-tooShortInitVector'] = [$key, $cipher, $mode, $tooShortInitVector]; + $result[$keyPrefix . '-tooLongInitVector'] = [$key, $cipher, $mode, $tooLongInitVector]; } } return $result; diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php index 74f2db9b05f2..f3853a123c15 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -3,13 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\Encryption\Test\Unit; +use Magento\Framework\Encryption\Adapter\Mcrypt; +use Magento\Framework\Encryption\Adapter\SodiumChachaIetf; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\Crypt; class EncryptorTest extends \PHPUnit\Framework\TestCase { + const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; + const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; + /** * @var \Magento\Framework\Encryption\Encryptor */ @@ -27,7 +35,7 @@ protected function setUp() $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue('cryptKey')); + ->will($this->returnValue(self::CRYPT_KEY_1)); $this->_model = new \Magento\Framework\Encryption\Encryptor($this->_randomGenerator, $deploymentConfigMock); } @@ -101,6 +109,7 @@ public function validateHashDataProvider() * @param mixed $key * * @dataProvider encryptWithEmptyKeyDataProvider + * @expectedException \SodiumException */ public function testEncryptWithEmptyKey($key) { @@ -155,20 +164,27 @@ public function testEncrypt() $actual = $this->_model->encrypt($data); // Extract the initialization vector and encrypted data - $parts = explode(':', $actual, 4); - list(, , $iv, $encryptedData) = $parts; + $parts = explode(':', $actual, 3); + list(, , $encryptedData) = $parts; - // Decrypt returned data with RIJNDAEL_256 cipher, cbc mode - $crypt = new Crypt('cryptKey', MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $iv); + $crypt = new SodiumChachaIetf(self::CRYPT_KEY_1); // Verify decrypted matches original data $this->assertEquals($data, $crypt->decrypt(base64_decode((string)$encryptedData))); } public function testDecrypt() + { + $message = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; + $encrypted = $this->_model->encrypt($message); + + $this->assertEquals($message, $this->_model->decrypt($encrypted)); + } + + public function testLegacyDecrypt() { // sample data to encrypt $data = '0:2:z3a4ACpkU35W6pV692U4ueCVQP0m0v0p:' . - '7ZPIIRZzQrgQH+csfF3fyxYNwbzPTwegncnoTxvI3OZyqKGYlOCTSx5i1KRqNemCC8kuCiOAttLpAymXhzjhNQ=='; + 'DhEG8/uKGGq92ZusqrGb6X/9+2Ng0QZ9z2UZwljgJbs5/A3LaSnqcK0oI32yjHY49QJi+Z7q1EKu2yVqB8EMpA=='; $actual = $this->_model->decrypt($data); @@ -177,7 +193,7 @@ public function testDecrypt() list(, , $iv, $encrypted) = $parts; // Decrypt returned data with RIJNDAEL_256 cipher, cbc mode - $crypt = new Crypt('cryptKey', MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $iv); + $crypt = new Crypt(self::CRYPT_KEY_1, MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $iv); // Verify decrypted matches original data $this->assertEquals($encrypted, base64_encode($crypt->encrypt($actual))); } @@ -188,11 +204,11 @@ public function testEncryptDecryptNewKeyAdded() $deploymentConfigMock->expects($this->at(0)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue("cryptKey1")); + ->will($this->returnValue(self::CRYPT_KEY_1)); $deploymentConfigMock->expects($this->at(1)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue("cryptKey1\ncryptKey2")); + ->will($this->returnValue(self::CRYPT_KEY_1 . "\n" . self::CRYPT_KEY_2)); $model1 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); // simulate an encryption key is being added $model2 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); @@ -208,12 +224,15 @@ public function testEncryptDecryptNewKeyAdded() public function testValidateKey() { - $actual = $this->_model->validateKey('some_key'); - $crypt = new Crypt('some_key', MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $actual->getInitVector()); - $expectedEncryptedData = base64_encode($crypt->encrypt('data')); - $actualEncryptedData = base64_encode($actual->encrypt('data')); - $this->assertEquals($expectedEncryptedData, $actualEncryptedData); - $this->assertEquals($crypt->decrypt($expectedEncryptedData), $actual->decrypt($actualEncryptedData)); + $this->_model->validateKey(self::CRYPT_KEY_1); + } + + /** + * @expectedException \Exception + */ + public function testValidateKeyInvalid() + { + $this->_model->validateKey('----- '); } /** diff --git a/lib/web/mage/adminhtml/browser.js b/lib/web/mage/adminhtml/browser.js index 9528e023d3ee..09ceafc011d6 100644 --- a/lib/web/mage/adminhtml/browser.js +++ b/lib/web/mage/adminhtml/browser.js @@ -434,7 +434,7 @@ define([ showLoader: true }).done($.proxy(function () { self.reload(); - self.element.find('#delete_files').toggleClass(self.options.hidden, true); + self.element.find('#delete_files, #insert_files').toggleClass(self.options.hidden, true); $(window).trigger('fileDeleted.mediabrowser', { ids: ids